diff --git a/.github/workflows/publish-nupkg.yml b/.github/workflows/publish-nupkg.yml new file mode 100644 index 0000000..dca1323 --- /dev/null +++ b/.github/workflows/publish-nupkg.yml @@ -0,0 +1,71 @@ +name: Build and Publish to Azure Artifacts / GitHub Packages + +on: + release: + types: [published] + +env: + AZURE_ARTIFACTS_FEED_URL: https://pkgs.dev.azure.com/intuitionps/01c84548-9607-4655-80e7-6ad95390a38c/_packaging/private/nuget/v3/index.json + GITHUB_PACKAGES_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + PROJECT_NAME: Ellucian.Ethos.Integration + BUILD_CONFIGURATION: 'Release' # set this to the appropriate build configuration + DOTNET_VERSION: '6.x' + +jobs: + build: + runs-on: windows-latest + steps: + # Checkout the repo + - uses: actions/checkout@v2 + + # Setup .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + # Run dotnet build and test + - name: dotnet build and test + run: | + dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} --name intuitionps --username ${{ github.actor }} --password ${{ secrets.GH_PACKAGES_PAT }} + dotnet nuget add source https://api.nuget.org/v3/index.json -name nugetorg + dotnet restore + dotnet build --configuration '${{ env.BUILD_CONFIGURATION }}' + dotnet test --configuration '${{ env.BUILD_CONFIGURATION }}' + + az-artifacts-build-and-deploy: + needs: build + runs-on: windows-latest + steps: + # Checkout the repo + - uses: actions/checkout@v2 + + # Extract git version + - name: Extract git tag + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + # Setup .NET Core SDK + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + source-url: ${{ env.AZURE_ARTIFACTS_FEED_URL }} + env: + NUGET_AUTH_TOKEN: ${{ secrets.AZURE_ARTIFACTS_PAT }} + + # Run dotnet build and package + - name: dotnet build and publish + run: | + dotnet nuget add source ${{ env.GITHUB_PACKAGES_URL }} --name intuitionps --username ${{ github.actor }} --password ${{ secrets.GH_PACKAGES_PAT }} + dotnet nuget add source https://api.nuget.org/v3/index.json -name nugetorg + dotnet restore + dotnet build --configuration '${{ env.BUILD_CONFIGURATION }}' /p:Version=${{ github.event.release.tag_name }} + dotnet pack -c '${{ env.BUILD_CONFIGURATION }}' + + # Publish the package to Azure Artifacts | must specify our version since the ellucian-developer/integration-sdk-csharp gets packaged as well + - name: 'dotnet publish to Azure Artifacts' + run: dotnet nuget push --api-key AzureArtifacts ${{ env.PROJECT_NAME }}\bin\Release\${{ env.PROJECT_NAME }}.${{ github.event.release.tag_name }}.nupkg + + # Publish the package to GitHub Packages | must specify our version since the ellucian-developer/integration-sdk-csharp gets packaged as well + - name: 'dotnet publish to GitHub Packages' + run: dotnet nuget push --api-key ${{ secrets.GH_PACKAGES_PAT }} --source ${{ env.GITHUB_PACKAGES_URL }} ${{ env.PROJECT_NAME }}\bin\Release\${{ env.PROJECT_NAME }}.${{ github.event.release.tag_name }}.nupkg diff --git a/ColleagueApiExample/ColleagueApiExample.csproj b/ColleagueApiExample/ColleagueApiExample.csproj new file mode 100644 index 0000000..8100c8c --- /dev/null +++ b/ColleagueApiExample/ColleagueApiExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/ColleagueApiExample/Program.cs b/ColleagueApiExample/Program.cs new file mode 100644 index 0000000..717c212 --- /dev/null +++ b/ColleagueApiExample/Program.cs @@ -0,0 +1,37 @@ +using Ellucian.Ethos.Integration.Client; +using Ellucian.Ethos.Integration.Client.Filter.Extensions; +using Ellucian.Ethos.Integration.Client.Proxy.Filter; +using Newtonsoft.Json.Linq; + +var proxyClient = + new EthosClientBuilder( + colleagueApiUrl: "", + colleagueApiUsername: "", + colleagueApiPassword: "") + .BuildColleagueWebApiProxyclient(); + +var academicPeriod = + await proxyClient.GetAsJObjectByIdAsync("academic-periods", "a4b5fddc-fa2f-4e94-82e9-cbe219a5029b"); + +Console.WriteLine(academicPeriod.ToString()); + +var queryClient = + new EthosClientBuilder( + colleagueApiUrl: "", + colleagueApiUsername: "", + colleagueApiPassword: "") + .BuildColleagueWebApiFilterQueryClient(); + +var filter = + new CriteriaFilter() + .WithSimpleCriteria("startOn", ("$gte", "2020-01-01")); + +var responses = + await queryClient.GetPagesWithCriteriaFilterAsync("academic-periods", filter); + +foreach (var response in responses) +{ + var acadPeriods = JArray.Parse(response.Content); + + Console.WriteLine(acadPeriods.ToString()); +} \ No newline at end of file diff --git a/Ellucian.Ethos.Integration.sln b/Ellucian.Ethos.Integration.sln index 3c713e7..e93ce60 100644 --- a/Ellucian.Ethos.Integration.sln +++ b/Ellucian.Ethos.Integration.sln @@ -17,6 +17,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{80815184 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{1402F60B-E306-4A41-B41E-2EF9136DFB36}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{B29BB850-080B-4A04-AF80-BB1B3FD91BC9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColleagueApiExample", "ColleagueApiExample\ColleagueApiExample.csproj", "{41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +55,18 @@ Global {DC26CC20-E923-4AA4-93C2-BCBEFC10EF26}.Release|x64.Build.0 = Release|Any CPU {DC26CC20-E923-4AA4-93C2-BCBEFC10EF26}.Release|x86.ActiveCfg = Release|Any CPU {DC26CC20-E923-4AA4-93C2-BCBEFC10EF26}.Release|x86.Build.0 = Release|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Debug|x64.Build.0 = Debug|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Debug|x86.Build.0 = Debug|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Release|Any CPU.Build.0 = Release|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Release|x64.ActiveCfg = Release|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Release|x64.Build.0 = Release|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Release|x86.ActiveCfg = Release|Any CPU + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -58,6 +74,7 @@ Global GlobalSection(NestedProjects) = preSolution {34B4B30E-C0E0-43E9-88D0-27B31BA01574} = {1402F60B-E306-4A41-B41E-2EF9136DFB36} {DC26CC20-E923-4AA4-93C2-BCBEFC10EF26} = {80815184-6446-4B0F-B7A4-B190E5E53F70} + {41EC6B03-53B4-43FC-AA8B-0E74C143F2FD} = {B29BB850-080B-4A04-AF80-BB1B3FD91BC9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB6A0A9A-4183-491A-97B0-86939AB919E1} diff --git a/Ellucian.Ethos.Integration/Authentication/SupportedRegions.cs b/Ellucian.Ethos.Integration/Authentication/SupportedRegions.cs index 11403ac..047da63 100644 --- a/Ellucian.Ethos.Integration/Authentication/SupportedRegions.cs +++ b/Ellucian.Ethos.Integration/Authentication/SupportedRegions.cs @@ -27,6 +27,10 @@ public enum SupportedRegions /// /// Europe. /// - Europe - } + Europe, + /// + /// Self-Hosted. + /// + SelfHosted + } } \ No newline at end of file diff --git a/Ellucian.Ethos.Integration/Client/EthosClient.cs b/Ellucian.Ethos.Integration/Client/EthosClient.cs index f2b0f8d..8b49901 100644 --- a/Ellucian.Ethos.Integration/Client/EthosClient.cs +++ b/Ellucian.Ethos.Integration/Client/EthosClient.cs @@ -28,6 +28,24 @@ public class EthosClient /// private string ApiKey { get; } + // protected EthosEthosIntegrationUrls EthosIntegrationUrls = new EthosEthosIntegrationUrls(); + + // Only used by ColleagueWebAPIProxyClients + /// + /// Api URL to Self-Hosted Colleague API. + /// + protected string ColleagueApiUrl; + /// + /// Self-Hosted Colleague API username. + /// + protected string ColleagueApiUsername; + /// + /// Self-Hosted Colleague API password. + /// + protected string ColleagueApiPassword; + + + /// /// Default token expiration time. /// @@ -102,6 +120,31 @@ public EthosClient( string apiKey, HttpClient client ) this.HttpProtocolClientBuilder ??= new HttpProtocolClientBuilder( client ); EthosResponseBuilder ??= new EthosResponseBuilder(); } + /// + /// Constructor called by subclasses of this class, specifically for ColleagueWebApiProxies. + /// + /// The URL to the Colleague API instance. If it is null/empty, then an will be thrown. + /// The username used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// The password used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// A . + public EthosClient(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword, HttpClient client) + { + if (colleagueApiUrl == null || colleagueApiUsername == null + || colleagueApiPassword == null || colleagueApiPassword == null) + { + throw new ArgumentNullException($"Colleague API URL and Credentials are required."); + } + if (client == null) + { + throw new ArgumentNullException($"The '{nameof(client)}' parameter is required."); + } + ApiKey = null; + this.ColleagueApiUrl = colleagueApiUrl; + this.ColleagueApiUsername = colleagueApiUsername; + this.ColleagueApiPassword = colleagueApiPassword; + this.HttpProtocolClientBuilder ??= new HttpProtocolClientBuilder(client); + EthosResponseBuilder ??= new EthosResponseBuilder(); + } #endregion @@ -175,7 +218,7 @@ public async Task GetAccessTokenAsync() /// Returns exception if the request fails. private async Task GetNewTokenAsync() { - string authUrl = $"{ EthosIntegrationUrls.Auth( this.Region )}?expirationMinutes={ ExpirationMinutes }"; + string authUrl = $"{EthosIntegrationUrls.Auth( this.Region )}?expirationMinutes={ ExpirationMinutes }"; HttpProtocolClientBuilder.Client.DefaultRequestHeaders.Add( "Authorization", $"Bearer { ApiKey }" ); //make request HttpResponseMessage response = await HttpProtocolClientBuilder.Client.PostAsync( new Uri( authUrl ), null ); @@ -369,11 +412,20 @@ public async Task DeleteAsync( Dictionary headers, string url ) /// A private async Task AddAccessTokenAuthHeaderAsync( Dictionary headers ) { - AccessToken token = await GetAccessTokenAsync(); - if ( token.GetAuthHeader().TryGetValue( "Authorization", out string authValue ) ) + headers.Remove("Authorization"); + if (Region == SupportedRegions.SelfHosted) + { + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(ColleagueApiUsername + ":" + ColleagueApiPassword); + string token = System.Convert.ToBase64String(bytes); + headers.Add("Authorization", "Basic " + token); + } + else { - headers.Remove( "Authorization" ); - headers.Add( "Authorization", authValue ); + AccessToken token = await GetAccessTokenAsync(); + if (token.GetAuthHeader().TryGetValue("Authorization", out string authValue)) + { + headers.Add("Authorization", authValue); + } } } diff --git a/Ellucian.Ethos.Integration/Client/EthosClientBuilder.cs b/Ellucian.Ethos.Integration/Client/EthosClientBuilder.cs index 401c493..587abc2 100644 --- a/Ellucian.Ethos.Integration/Client/EthosClientBuilder.cs +++ b/Ellucian.Ethos.Integration/Client/EthosClientBuilder.cs @@ -28,6 +28,10 @@ public class EthosClientBuilder /// private int? ConnectionTimeout = null; + private readonly string ColleagueApiUrl; + private readonly string ColleagueApiUsername; + private readonly string ColleagueApiPassword; + /// /// Interface used in HttpProtocolClientBuilder. /// @@ -42,6 +46,19 @@ public EthosClientBuilder( string apiKey ) this.apiKey = apiKey; builder ??= new HttpProtocolClientBuilder( null, ConnectionTimeout ); } + /// + /// Constructs this class with the given Colleauge API URL/Credentials. + /// + /// The URL to the Colleague API instance. + /// The username used to connect to the Colleague API. + /// The password used to connect to the Colleague API. + public EthosClientBuilder(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword) + { + builder ??= new HttpProtocolClientBuilder(null, ConnectionTimeout); + ColleagueApiUrl = colleagueApiUrl; + ColleagueApiUsername = colleagueApiUsername; + ColleagueApiPassword = colleagueApiPassword; + } /// /// Give the client factory a connection timeout so that connections will time out after connectionTimeout @@ -97,7 +114,25 @@ public EthosMessagesClient BuildEthosMessagesClient() /// An EthosFilterQueryClient using the given apiKey and timeout values. public EthosFilterQueryClient BuildEthosFilterQueryClient() { - return new EthosFilterQueryClient( apiKey, builder.Client ); + return new EthosFilterQueryClient(apiKey, builder.Client); + } + + /// + /// Gets an that will use the given Colleague credentials to authenticate. + /// + /// An ColleagueWebApiProxyClient using the given Colleague credentials. + public ColleagueWebApiProxyClient BuildColleagueWebApiProxyclient() + { + return new ColleagueWebApiProxyClient(ColleagueApiUrl, ColleagueApiUsername, ColleagueApiPassword, builder.Client); + } + + /// + /// Gets an that will use the Colleague credentials to authenticate. + /// + /// An ColleagueWebApiFilterQueryClient using the given Colleague credentials. + public ColleagueWebApiFilterQueryClient BuildColleagueWebApiFilterQueryClient() + { + return new ColleagueWebApiFilterQueryClient(ColleagueApiUrl, ColleagueApiUsername, ColleagueApiPassword, builder.Client); } } } diff --git a/Ellucian.Ethos.Integration/Client/HttpProtocolClientBuilder.cs b/Ellucian.Ethos.Integration/Client/HttpProtocolClientBuilder.cs index 1c138a8..01634df 100644 --- a/Ellucian.Ethos.Integration/Client/HttpProtocolClientBuilder.cs +++ b/Ellucian.Ethos.Integration/Client/HttpProtocolClientBuilder.cs @@ -24,9 +24,9 @@ public class HttpProtocolClientBuilder : IHttpProtocolClientBuilder /// Time in seconds to allow an http connection to time out. Default is /// 300 seconds (5 minutes). /// - private static int CONNECTION_TIMEOUT = 300; + private static readonly int CONNECTION_TIMEOUT = 300; - private const SslProtocols PROTOCOL = SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11; + private const SslProtocols PROTOCOL = SslProtocols.Tls13 | SslProtocols.Tls12; private const string CLIENT_NAME = "EllucianEthosIntegrationSdk-dotnet"; @@ -44,10 +44,7 @@ public class HttpProtocolClientBuilder : IHttpProtocolClientBuilder /// 300 seconds (5 minutes). public HttpProtocolClientBuilder( HttpClient client, int? connectionTimeOut = null ) { - if ( client == null ) - { - client = BuildHttpClient( connectionTimeOut.HasValue ? connectionTimeOut : CONNECTION_TIMEOUT ); - } + client ??= BuildHttpClient( connectionTimeOut.HasValue ? connectionTimeOut : CONNECTION_TIMEOUT ); Client = client; } @@ -66,7 +63,7 @@ public HttpClient BuildHttpClient( int? connectionTimeout ) client.DefaultRequestHeaders.Clear(); client.DefaultRequestHeaders.Add( "pragma", "no-cache" ); client.DefaultRequestHeaders.Add( "cache-control", "no-cache" ); - ProductInfoHeaderValue prodHeaderVal = new ProductInfoHeaderValue( CLIENT_NAME, Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ); + ProductInfoHeaderValue prodHeaderVal = new( CLIENT_NAME, Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ); client.DefaultRequestHeaders.UserAgent.Add( prodHeaderVal ); client.Timeout = TimeSpan.FromMinutes( ( double ) connectionTimeout ); } ) diff --git a/Ellucian.Ethos.Integration/Client/Messages/EthosMessagesClient.cs b/Ellucian.Ethos.Integration/Client/Messages/EthosMessagesClient.cs index 07b3a34..08458bd 100644 --- a/Ellucian.Ethos.Integration/Client/Messages/EthosMessagesClient.cs +++ b/Ellucian.Ethos.Integration/Client/Messages/EthosMessagesClient.cs @@ -113,7 +113,7 @@ private async Task> GetMessagesAsync( int? limit /// The number of available messages in the application's queue. public async Task GetNumAvailableMessagesAsync() { - EthosResponse response = await HeadAsync( EthosIntegrationUrls.Consume( Region, -1, -1 ) ); + EthosResponse response = await HeadAsync(EthosIntegrationUrls.Consume( Region, -1, -1 ) ); string remaining = response.GetHeader( "x-remaining" ); if ( int.TryParse( remaining, out int numMessages ) ) { diff --git a/Ellucian.Ethos.Integration/Client/Proxy/ColleagueWebApiFilterQueryClient.cs b/Ellucian.Ethos.Integration/Client/Proxy/ColleagueWebApiFilterQueryClient.cs new file mode 100644 index 0000000..23dff29 --- /dev/null +++ b/Ellucian.Ethos.Integration/Client/Proxy/ColleagueWebApiFilterQueryClient.cs @@ -0,0 +1,40 @@ +/* + * ****************************************************************************** + * Copyright 2022 Ellucian Company L.P. and its affiliates. + * ****************************************************************************** + */ + +using Ellucian.Ethos.Integration.Client.Filter.Extensions; +using Ellucian.Ethos.Integration.Client.Proxy.Filter; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Ellucian.Ethos.Integration.Client.Proxy +{ + /// + /// An EthosProxyClient that provides the ability to submit GET requests supporting filters and/or named queries with support for paging. + /// + public class ColleagueWebApiFilterQueryClient : EthosFilterQueryClient + { + /// + /// Instantiates this class using the given Colleague API url and credentials. + /// + /// The URL to the Colleague API instance. If it is null/empty, then an will be thrown. + /// The username used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// The password used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// A HttpClient. If it is null/empty, then an will be thrown. + public ColleagueWebApiFilterQueryClient(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword, HttpClient client) + : base(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword, client) + { + Region = Authentication.SupportedRegions.SelfHosted; + EthosIntegrationUrls.SelfHostBaseUrl = colleagueApiUrl; + } + } +} diff --git a/Ellucian.Ethos.Integration/Client/Proxy/ColleagueWebApiProxyClient.cs b/Ellucian.Ethos.Integration/Client/Proxy/ColleagueWebApiProxyClient.cs new file mode 100644 index 0000000..00f4557 --- /dev/null +++ b/Ellucian.Ethos.Integration/Client/Proxy/ColleagueWebApiProxyClient.cs @@ -0,0 +1,35 @@ +using Ellucian.Ethos.Integration.Client.Filter.Extensions; +using Ellucian.Ethos.Integration.Client.Proxy.Filter; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Ellucian.Ethos.Integration.Client.Proxy +{ + /// + /// An EthosProxyClient that provides the ability to submit GET requests supporting filters and/or named queries with support for paging. + /// + public class ColleagueWebApiProxyClient : EthosProxyClient + { + /// + /// Instantiates this class using the given Colleague API url and credentials. + /// + /// The URL to the Colleague API instance. If it is null/empty, then an will be thrown. + /// The username used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// The password used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// A HttpClient. If it is null/empty, then an will be thrown. + public ColleagueWebApiProxyClient( string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword, HttpClient client ) + : base(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword, client) + { + Region = Authentication.SupportedRegions.SelfHosted; + EthosIntegrationUrls.SelfHostBaseUrl = colleagueApiUrl; + } + + } +} diff --git a/Ellucian.Ethos.Integration/Client/Proxy/EthosFilterQueryClient.cs b/Ellucian.Ethos.Integration/Client/Proxy/EthosFilterQueryClient.cs index d28d933..91b2180 100644 --- a/Ellucian.Ethos.Integration/Client/Proxy/EthosFilterQueryClient.cs +++ b/Ellucian.Ethos.Integration/Client/Proxy/EthosFilterQueryClient.cs @@ -36,6 +36,18 @@ public class EthosFilterQueryClient : EthosProxyClient public EthosFilterQueryClient( string apiKey, HttpClient client ) : base( apiKey, client ) { + } + /// + /// Instantiates this class using the given Colleague URL and credentials and HttpClient. + /// + /// The URL to the Colleague API instance. If it is null/empty, then an will be thrown. + /// The username used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// The password used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// A HttpClient. If it is null/empty, then an will be thrown. + public EthosFilterQueryClient(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword, HttpClient client) + : base(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword, client) + { + } #region Strongly Typed GET diff --git a/Ellucian.Ethos.Integration/Client/Proxy/EthosProxyClient.cs b/Ellucian.Ethos.Integration/Client/Proxy/EthosProxyClient.cs index 5025adb..53dbbb3 100644 --- a/Ellucian.Ethos.Integration/Client/Proxy/EthosProxyClient.cs +++ b/Ellucian.Ethos.Integration/Client/Proxy/EthosProxyClient.cs @@ -123,6 +123,18 @@ public class EthosProxyClient : EthosClient public EthosProxyClient( string apiKey, HttpClient client ) : base( apiKey, client ) { + } + /// + /// Constructs an EthosProxyClient using the given Colleague API URL and credentials. + /// + /// The URL to the Colleague API instance. If it is null/empty, then an will be thrown. + /// The username used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// The password used to connect to the Colleague API. If it is null/empty, then an will be thrown. + /// A . + public EthosProxyClient(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword, HttpClient client) + : base(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword, client) + { + } #region POST @@ -553,7 +565,7 @@ internal IEnumerable ConvertEthosResponseContentListToType( IE var version = DEFAULT_VERSION; Dictionary headers = BuildHeadersMap( version ); - string url = $"{ EthosIntegrationUrls.Api( Region, resourceName ) }"; + string url = $"{EthosIntegrationUrls.Api( Region, resourceName ) }"; EthosResponse response = await GetAsync( headers, url ); return response; } @@ -570,7 +582,7 @@ public async Task GetAsync( string resourceName, string version = if ( string.IsNullOrWhiteSpace( resourceName ) ) { throw new ArgumentNullException( nameof( resourceName ) ); } Dictionary headers = BuildHeadersMap( version ); - string url = $"{ EthosIntegrationUrls.Api( Region, resourceName ) }"; + string url = $"{EthosIntegrationUrls.Api( Region, resourceName ) }"; EthosResponse response = await GetAsync( headers, url ); return response; } @@ -618,7 +630,7 @@ public async Task GetAsync( string resourceName, string version = if ( string.IsNullOrWhiteSpace( resourceName ) ) { throw new ArgumentNullException( nameof( resourceName ) ); } Dictionary headers = BuildHeadersMap( version ); - string url = $"{ EthosIntegrationUrls.ApiPaging( Region, resourceName, offset, pageSize ) }"; + string url = $"{EthosIntegrationUrls.ApiPaging( Region, resourceName, offset, pageSize ) }"; EthosResponse response = await GetAsync( headers, url ); return response; } diff --git a/Ellucian.Ethos.Integration/Ellucian.Ethos.Integration.csproj b/Ellucian.Ethos.Integration/Ellucian.Ethos.Integration.csproj index 59d7ef3..0d21884 100644 --- a/Ellucian.Ethos.Integration/Ellucian.Ethos.Integration.csproj +++ b/Ellucian.Ethos.Integration/Ellucian.Ethos.Integration.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 Ellucian.Ethos.Integration 1.0.0 Ellucian Ethos Integration Team @@ -31,6 +31,7 @@ The Ethos Integration SDK makes the application development process less expensi README.md True https://github.com/ellucian-developer/integration-sdk-csharp + true @@ -58,9 +59,9 @@ The Ethos Integration SDK makes the application development process less expensi - - - + + + diff --git a/Ellucian.Ethos.Integration/EthosIntegrationUrls.cs b/Ellucian.Ethos.Integration/EthosIntegrationUrls.cs index a1206a8..78bf4f2 100644 --- a/Ellucian.Ethos.Integration/EthosIntegrationUrls.cs +++ b/Ellucian.Ethos.Integration/EthosIntegrationUrls.cs @@ -33,6 +33,9 @@ public static class EthosIntegrationUrls /// /// AUSTRALIA: .com.au /// + /// + /// SELF-HOSTED + /// /// /// private static readonly Dictionary RegionUrlPostFix = new Dictionary @@ -40,11 +43,29 @@ public static class EthosIntegrationUrls [ SupportedRegions.US ] = ".com", [ SupportedRegions.Canada ] = ".ca", [ SupportedRegions.Europe ] = ".ie", - [ SupportedRegions.Australia ] = ".com.au" + [ SupportedRegions.Australia ] = ".com.au", + [ SupportedRegions.SelfHosted ] = "" }; + +#pragma warning disable S1075 + const string MAIN_ETHOS_BASE_URL = "https://integrate.elluciancloud"; +#pragma warning restore S1075 + + /// The override for self-hosted ERP clients. + public static string SelfHostBaseUrl { get; set; } = ""; + ///The main domain for Ethos Integration. - private const string MAIN_BASE_URL = "https://integrate.elluciancloud"; + public static string MAIN_BASE_URL + { + get + { + if (string.IsNullOrWhiteSpace(SelfHostBaseUrl)) + return MAIN_ETHOS_BASE_URL; + + return SelfHostBaseUrl; + } + } /// /// The base URL for getting Api result(s) in Ethos Integration. @@ -55,13 +76,13 @@ public static class EthosIntegrationUrls /// A string value containing the URL to use for interacting with Ethos Integration Proxy APIs. public static string Api( SupportedRegions region, string resource, string id = "" ) { - string url = BuildUrl( region, "/api" ); - if ( !string.IsNullOrWhiteSpace( resource ) ) + string url = BuildUrl(region, "/api"); + if (!string.IsNullOrWhiteSpace(resource)) { - url += ( "/" + resource ); - if ( !string.IsNullOrWhiteSpace( id ) ) + url += ("/" + resource); + if (!string.IsNullOrWhiteSpace(id)) { - url += ( "/" + id ); + url += ("/" + id); } } return url; @@ -245,12 +266,14 @@ public static string BaseUrl( SupportedRegions region ) /// Builds the URL with the mainBaseUrl, the supported region, and the correct path. /// /// The appropriate supported region to build the URL with. - /// The correct path for the type of API the URL will be used with (/api for Proxy API URL, - /// for Token API URL, etc.). + /// The correct path for the type of API the URL will be used with (/api for Proxy API URL, + /// for Token API URL, etc.). /// - private static string BuildUrl( SupportedRegions region, string urlEnd ) + private static string BuildUrl( SupportedRegions region, string urlEnd) { - return $"{MAIN_BASE_URL}{RegionUrlPostFix [ region ]}{urlEnd}"; + return region == SupportedRegions.SelfHosted + ? $"{MAIN_BASE_URL}" + : $"{MAIN_BASE_URL}{RegionUrlPostFix[region]}{urlEnd}"; } /// diff --git a/Ellucian.Ethos.Integration/Service/EthosChangeNotificationService.cs b/Ellucian.Ethos.Integration/Service/EthosChangeNotificationService.cs index c0187bc..2fb9431 100644 --- a/Ellucian.Ethos.Integration/Service/EthosChangeNotificationService.cs +++ b/Ellucian.Ethos.Integration/Service/EthosChangeNotificationService.cs @@ -35,7 +35,20 @@ public class EthosChangeNotificationService : EthosService /// This constructor is only called from the inner Builder class. /// /// A api key. - private EthosChangeNotificationService( string apiKey ) : this( new EthosClientBuilder( apiKey ) ) + private EthosChangeNotificationService(string apiKey) + : this( new EthosClientBuilder(apiKey)) + { + + } + + /// + /// Instantiates this service class with Colleague API and credentials. + /// + /// The URL to the Colleague API instance. + /// The username used to connect to the Colleague API. + /// The password used to connect to the Colleague API. + private EthosChangeNotificationService(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword) + : this( new EthosClientBuilder(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword)) { } @@ -333,12 +346,25 @@ private void BuildService( EthosClientBuilder ethosClientBuilder ) /// Actions delegate. /// A api key. /// An instance of the EthosChangeNotificationService. - public static EthosChangeNotificationService Build( Action action, string apiKey ) + public static EthosChangeNotificationService Build( Action action, string apiKey) { - EthosChangeNotificationService ethosChangeNotificationService = new EthosChangeNotificationService( apiKey ); + EthosChangeNotificationService ethosChangeNotificationService = new EthosChangeNotificationService(apiKey); action( ethosChangeNotificationService ); return ethosChangeNotificationService; } + /// + /// Builds an instance of the EthosChangeNotificationService with the given ethosClientBuilder and any resource version overrides. + /// + /// Actions delegate. + /// The URL to the Colleague API instance. + /// The username used to connect to the Colleague API. + /// The password used to connect to the Colleague API. + public static EthosChangeNotificationService Build(Action action, string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword) + { + EthosChangeNotificationService ethosChangeNotificationService = new EthosChangeNotificationService(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword); + action(ethosChangeNotificationService); + return ethosChangeNotificationService; + } /// /// Adds resource name and version to a dictionary. If the key (resourceName) already exists with version value, then it replaces same resource with the version provided. diff --git a/Ellucian.Ethos.Integration/Service/EthosService.cs b/Ellucian.Ethos.Integration/Service/EthosService.cs index 414b277..d412fb0 100644 --- a/Ellucian.Ethos.Integration/Service/EthosService.cs +++ b/Ellucian.Ethos.Integration/Service/EthosService.cs @@ -26,9 +26,20 @@ protected EthosService() /// Constructs this service with the given API key. /// /// apiKey The API key used by the EthosClients of this service when obtaining an access token per request. - protected EthosService( string apiKey ) + protected EthosService(string apiKey) { - EthosClientBuilder = new EthosClientBuilder( apiKey ); + EthosClientBuilder = new EthosClientBuilder(apiKey); + } + + /// + /// Constructs this service with the given Colleague API and credentials. + /// + /// The URL to the Colleague API instance. + /// The username used to connect to the Colleague API. + /// The password used to connect to the Colleague API. + protected EthosService(string colleagueApiUrl, string colleagueApiUsername, string colleagueApiPassword) + { + EthosClientBuilder = new EthosClientBuilder(colleagueApiUrl, colleagueApiUsername, colleagueApiPassword); } /// diff --git a/Ellucian.Ethos.Integration/packages.lock.json b/Ellucian.Ethos.Integration/packages.lock.json new file mode 100644 index 0000000..e772b8d --- /dev/null +++ b/Ellucian.Ethos.Integration/packages.lock.json @@ -0,0 +1,135 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Http": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index e0ddd36..33243f0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ # Ellucian Ethos Integration SDK +> Modifications by Intuition Payment Solutions + +![Azure DevOps Release Pipeline Status](https://vsrm.dev.azure.com/intuitionps/_apis/public/Release/badge/01c84548-9607-4655-80e7-6ad95390a38c/18/46) + +[![Build and Publish to Azure Artifacts / GitHub Packages](https://github.com/iNtuitionPS/ethos-integration-sdk-fork/actions/workflows/publish-nupkg.yml/badge.svg)](https://github.com/iNtuitionPS/ethos-integration-sdk-fork/actions/workflows/publish-nupkg.yml) + +| Branch | Build Pipeline Status | +|:---|:---| +| `main` | | +| `developmment` | N/A | + +# Ethos Integration SDK provides utilities and libraries that make it easier for developers to quickly start building Ethos-based integrations. diff --git a/_azure-pipelines-CI.yml b/_azure-pipelines-CI.yml new file mode 100644 index 0000000..2d1efdd --- /dev/null +++ b/_azure-pipelines-CI.yml @@ -0,0 +1,180 @@ +# Build Pipeline - Continuous Integration +# if commit is not from a pull request, then the pipeline will abort - bypass this check by setting the SkipPullRequestCheck variable at queue time + +trigger: +- main + +pr: none + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages + +steps: +- checkout: self + persistCredentials: true + +- task: PowerShell@2 + displayName: Get Pull Request + condition: and(succeeded(), eq(variables['SkipPullRequestCheck'], '')) + inputs: + targetType: 'inline' + script: | + try { + $headers = @{ "Authorization" = "Bearer $(GitHubToken)"} + $uri = 'https://api.github.com/repos/$(Build.Repository.Name)/commits/$(Build.SourceVersion)/pulls' + Write-Output "uri = $uri" + $response = Invoke-WebRequest -Uri $uri -Headers $headers -Method Get + $content = $response.content | ConvertFrom-Json + # Check if the $content is not empty and iterate through each pull request + if (-not ($content -and $content.Count -gt 0)) { + Write-Host "##vso[task.logissue type=error;]Failed: Commit is not from a pull request." + exit 1 + } + $matchedPullRequest = $null + foreach ($pullRequest in $content) { + if ($pullRequest.merge_commit_sha -eq "$(Build.SourceVersion)") { + $matchedPullRequest = $pullRequest + break + } + } + # Check if a matching pull request was found + if (-not $matchedPullRequest) { + Write-Host "##vso[task.logissue type=error;]Failed: No pull request found with merge commit SHA = '$(Build.SourceVersion)'." + exit 1 + } + # If a matching pull request is found, set the variables and continue + Write-Host "##vso[task.setvariable variable=pullRequestHeadRef;]$($matchedPullRequest.head.ref)" + Write-Host "##vso[task.setvariable variable=pullRequestBaseRef;]$($matchedPullRequest.base.ref)" + } catch { + Write-Host "##vso[task.logissue type=error;]Error getting pull request - | $_" + exit 1 + } + +- task: PowerShell@2 + displayName: Reset Development Branch + condition: and(succeeded(), eq(variables['SkipPullRequestCheck'], '')) + inputs: + targetType: 'inline' + script: | + if ("$(pullRequestHeadRef)" -eq "development" -and "$(pullRequestBaseRef)" -eq "main") { + # Define required headers for Azure DevOps | url is pulled from 'common' variable group in library + $azureDevOpsAuthenicationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$(AzureDevOpsToken)")); "Content-Type" = "application/json"} + # Define the base URL for Azure DevOps API + $azureDevOpsRequestUri = "$(AzureDevOpsBuildApiUrl)/_apis/build/definitions/$(System.DefinitionId)?api-version=7.0" + try { + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f "", "$(GitHubToken)"))) + # Get the current pipeline definition + $current_definition = Invoke-RestMethod -Method Get -Uri $azureDevOpsRequestUri -Headers $azureDevOpsAuthenicationHeader + # Disable build pipeline + $current_definition.queueStatus = "disabled" + $disabled_definition = $current_definition | ConvertTo-Json -Depth 100 + Invoke-RestMethod -Method Put -Uri $azureDevOpsRequestUri -Headers $azureDevOpsAuthenicationHeader -Body $disabled_definition + # Perform the reset + git config user.email "no-reply@intuitionps.com" + git config user.name "intuitionps Azure DevOps" + git checkout development + if ($LASTEXITCODE -ne 0) { throw "git checkout development" } + git fetch origin + if ($LASTEXITCODE -ne 0) { throw "git fetch origin" } + git reset --hard origin/main + if ($LASTEXITCODE -ne 0) { throw "git reset --hard origin/main" } + git push --force origin development + if ($LASTEXITCODE -ne 0) { throw "git push --force origin development" } + Write-Host "Successfully reset development branch from main branch." + } catch { + Write-Host "##vso[task.logissue type=error;]Error resetting development branch with main branch - | $_" + } finally { + try { + # Get the current pipeline definition + $current_definition = Invoke-RestMethod -Method Get -Uri $azureDevOpsRequestUri -Headers $azureDevOpsAuthenicationHeader + # Enable build pipeline + $current_definition.queueStatus = "enabled" + $enabled_definition = $current_definition | ConvertTo-Json -Depth 100 + Invoke-RestMethod -Method Put -Uri $azureDevOpsRequestUri -Headers $azureDevOpsAuthenicationHeader -Body $enabled_definition + } catch { + Write-Host "##vso[task.logissue type=error;]Error re-enabling build pipeline - | $_" + } + } + } else { + Write-Host "##vso[task.logissue type=warning;]Aborted: Only reset if base branch is 'main' and head branch is 'development'." + } + ignoreLASTEXITCODE: true + +- task: NuGetToolInstaller@1 + displayName: Install NuGet Tool + +- task: Cache@2 + displayName: Cache NuGet packages + inputs: + key: 'nuget | "$(Agent.OS)" | **/packages.lock.json,!**/bin/**,!**/obj/**' + restoreKeys: | + nuget | "$(Agent.OS)" + nuget + path: '$(NUGET_PACKAGES)' + +- task: NuGetCommand@2 + displayName: Restore Solution + inputs: + restoreSolution: '$(solution)' + feedsToUse: 'select' + vstsFeed: '01c84548-9607-4655-80e7-6ad95390a38c/3823d26d-e8ca-40c4-a6f3-c3bd13f9658b' + +- task: SonarQubePrepare@5 + displayName: SonarQube - Prepare Analysis Configuration + inputs: + SonarQube: 'SonarQube' + scannerMode: 'MSBuild' + projectKey: '$(SonarQubeProjectKey)' + +- task: VSBuild@1 + displayName: Build Solution + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: SonarQubeAnalyze@5 + displayName: SonarQube - Run Code Analysis + +- task: SonarQubePublish@5 + displayName: SonarQube - Publish Quality Gate Result + inputs: + pollingTimeoutSec: '300' + +- task: PowerShell@2 + displayName: SonarQube - Get Project Status + inputs: + targetType: 'inline' + script: | + try { + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$(SonarQubeToken):")) + } + $uri = 'https://ca-intuition-devops-sonarqube.azurewebsites.net/api/qualitygates/project_status?projectKey=$(SonarQubeProjectKey)&branch=$(Build.SourceBranchName)' + Write-Output "uri = $uri" + $response = Invoke-WebRequest -Uri $uri -Headers $headers -Method Get + $content = $response.Content | ConvertFrom-Json + $status = $content.projectStatus.status + Write-Output "SonarQube Project Status = $status" + if ($content.projectStatus.status -eq "ERROR") { + Write-Host "##vso[task.logissue type=error;]Failed: Code analysis did not pass SonarQube quality gate." + exit 1 + } + } catch { + Write-Host "##vso[task.logissue type=error;]Error getting project status - | $_" + exit 1 + } + +- task: PublishBuildArtifacts@1 + displayName: Publish Pipeline Artifacts + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'drop' + publishLocation: 'Container' diff --git a/_azure-pipelines-PR.yml b/_azure-pipelines-PR.yml new file mode 100644 index 0000000..d3d6698 --- /dev/null +++ b/_azure-pipelines-PR.yml @@ -0,0 +1,94 @@ +# Build Pipeline - Pull Request +# if target branch is 'main' and source branch is not 'development', then the pipeline will abort + +trigger: none + +pr: +- main +- development + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages + +steps: +- task: PowerShell@2 + displayName: Check Target Branch + inputs: + targetType: 'inline' + script: | + if ("$(system.pullRequest.targetBranch)" -eq "main" -and "$(system.pullRequest.sourceBranch)" -ne "development") { + Write-Host "##vso[task.logissue type=error;]Failed: Target branch is 'main' and source branch is not 'development'." + exit 1 + } + +- task: NuGetToolInstaller@1 + displayName: Install NuGet Tool + +- task: Cache@2 + displayName: Cache NuGet packages + inputs: + key: 'nuget | "$(Agent.OS)" | **/packages.lock.json,!**/bin/**,!**/obj/**' + restoreKeys: | + nuget | "$(Agent.OS)" + nuget + path: '$(NUGET_PACKAGES)' + +- task: NuGetCommand@2 + displayName: Restore Solution + inputs: + restoreSolution: '$(solution)' + feedsToUse: 'select' + vstsFeed: '01c84548-9607-4655-80e7-6ad95390a38c/3823d26d-e8ca-40c4-a6f3-c3bd13f9658b' + +- task: SonarQubePrepare@5 + displayName: Prepare Analysis Configuration + inputs: + SonarQube: 'SonarQube' + scannerMode: 'MSBuild' + projectKey: '$(SonarQubeProjectKey)' + +- task: VSBuild@1 + displayName: Build Solution + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: SonarQubeAnalyze@5 + displayName: SonarQube - Run Code Analysis + +- task: SonarQubePublish@5 + displayName: SonarQube - Publish Quality Gate Result + inputs: + pollingTimeoutSec: '300' + +- task: PowerShell@2 + displayName: SonarQube - Get Project Status + inputs: + targetType: 'inline' + script: | + try { + $headers = @{ + "Content-Type" = "application/json" + "Authorization" = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$(SonarQubeToken):")) + } + $uri = 'https://ca-intuition-devops-sonarqube.azurewebsites.net/api/qualitygates/project_status?projectKey=$(SonarQubeProjectKey)&pullRequest=$(system.pullRequest.pullRequestNumber)' + Write-Output "uri = $uri" + $response = Invoke-WebRequest -Uri $uri -Headers $headers -Method Get + $content = $response.Content | ConvertFrom-Json + $status = $content.projectStatus.status + Write-Output "SonarQube Project Status = $status" + if ($content.projectStatus.status -eq "ERROR") { + Write-Host "##vso[task.logissue type=error;]Failed: Code analysis did not pass SonarQube quality gate." + exit 1 + } + } catch { + Write-Host "##vso[task.logissue type=error;]Error getting project status - | $_" + exit 1 + } \ No newline at end of file