Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(GH-169) Add alias for accessing Azure Pipeline builds #170

Merged
merged 1 commit into from
Oct 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,003 changes: 1,003 additions & 0 deletions src/Cake.AzureDevOps.Tests/Build/AzureDevOpsBuildSettingsTests.cs

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions src/Cake.AzureDevOps/AzureDevOpsAliases.Build.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace Cake.AzureDevOps
{
using Cake.AzureDevOps.Build;
using Cake.Core;
using Cake.Core.Annotations;

/// <content>
/// Contains functionality related to Azure Pipeline builds.
/// </content>
public static partial class AzureDevOpsAliases
{
/// <summary>
/// Gets an Azure Pipelines build using the specified settings.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="settings">Settings for getting the build.</param>
/// <example>
/// <para>Get a pull request based on the source branch:</para>
/// <code>
/// <![CDATA[
/// var buildSettings =
/// new AzureDevOpsBuildSettings(
/// new Uri("http://myserver:8080/defaultcollection"),
/// "MyProject",
/// 42,
/// AzureDevOpsAuthenticationNtlm());
///
/// var build =
/// AzureDevOpsBuild(
/// buildSettings);
/// ]]>
/// </code>
/// </example>
/// <returns>Description of the build.
/// Returns <c>null</c> if build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.</returns>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
[CakeMethodAlias]
[CakeAliasCategory("Build")]
[CakeNamespaceImport("Cake.AzureDevOps.Build")]
public static AzureDevOpsBuild AzureDevOpsBuild(
this ICakeContext context,
AzureDevOpsBuildSettings settings)
{
context.NotNull(nameof(context));
settings.NotNull(nameof(settings));

var build = new AzureDevOpsBuild(context.Log, settings, new BuildClientFactory());

if (build.HasBuildLoaded)
{
return build;
}

return null;
}

/// <summary>
/// Gets the description of the Azure Pipelines build which is running.
/// Make sure the build has the 'Allow Scripts to access OAuth token' option enabled.
/// </summary>
/// <param name="context">The context.</param>
/// <example>
/// <para>Get current Azure Pipelines build:</para>
/// <code>
/// <![CDATA[
/// var build =
/// AzureDevOpsBuildUsingAzurePipelinesOAuthToken();
/// ]]>
/// </code>
/// </example>
/// <returns>Description of the build.
/// Returns <c>null</c> if build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.</returns>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
[CakeMethodAlias]
[CakeAliasCategory("Build")]
[CakeNamespaceImport("Cake.AzureDevOps.Build")]
public static AzureDevOpsBuild AzureDevOpsBuildUsingAzurePipelinesOAuthToken(
this ICakeContext context)
{
context.NotNull(nameof(context));

var settings = AzureDevOpsBuildSettings.UsingAzurePipelinesOAuthToken();

return AzureDevOpsBuild(context, settings);
}
}
}
232 changes: 232 additions & 0 deletions src/Cake.AzureDevOps/Build/AzureDevOpsBuild.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
namespace Cake.AzureDevOps.Build
{
using System;
using Cake.Core.Diagnostics;
using Microsoft.TeamFoundation.Build.WebApi;

/// <summary>
/// Class for writing issues to Azure DevOps pull requests.
/// </summary>
public sealed class AzureDevOpsBuild
{
private readonly ICakeLog log;
private readonly bool throwExceptionIfBuildCouldNotBeFound;
private readonly IBuildClientFactory buildClientFactory;
private readonly Build build;

/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuild"/> class.
/// </summary>
/// <param name="log">The Cake log context.</param>
/// <param name="settings">Settings for accessing AzureDevOps.</param>
/// <exception cref="AzureDevOpsBuildNotFoundException">If <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/>
/// is set to <c>true</c> and no build could be found.</exception>
public AzureDevOpsBuild(ICakeLog log, AzureDevOpsBuildSettings settings)
: this(log, settings, new BuildClientFactory())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuild"/> class.
/// </summary>
/// <param name="log">The Cake log context.</param>
/// <param name="settings">Settings for accessing AzureDevOps.</param>
/// <param name="buildClientFactory">A factory to communicate with Build client.</param>
/// <exception cref="AzureDevOpsBuildNotFoundException">If <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/>
/// is set to <c>true</c> and no build could be found.</exception>
internal AzureDevOpsBuild(ICakeLog log, AzureDevOpsBuildSettings settings, IBuildClientFactory buildClientFactory)
{
log.NotNull(nameof(log));
settings.NotNull(nameof(settings));
buildClientFactory.NotNull(nameof(buildClientFactory));

this.log = log;
this.buildClientFactory = buildClientFactory;
this.CollectionUrl = settings.CollectionUrl;
this.throwExceptionIfBuildCouldNotBeFound = settings.ThrowExceptionIfBuildCouldNotBeFound;

using (var buildClient = this.buildClientFactory.CreateBuildClient(settings.CollectionUrl, settings.Credentials, out var authorizedIdenity))
{
this.log.Verbose(
"Authorized Identity:\n Id: {0}\n DisplayName: {1}",
authorizedIdenity.Id,
authorizedIdenity.DisplayName);

try
{
if (settings.ProjectGuid != Guid.Empty)
{
this.log.Verbose("Read build with ID {0} from project with ID {1}", settings.BuildId, settings.ProjectGuid);
this.build =
buildClient.GetBuildAsync(
settings.ProjectGuid,
settings.BuildId).GetAwaiter().GetResult();
}
else if (!string.IsNullOrWhiteSpace(settings.ProjectName))
{
this.log.Verbose("Read build with ID {0} from project with name {1}", settings.BuildId, settings.ProjectName);
this.build =
buildClient.GetBuildAsync(
settings.ProjectName,
settings.BuildId).GetAwaiter().GetResult();
}
else
{
throw new ArgumentOutOfRangeException(
nameof(settings),
"Either ProjectGuid or ProjectName needs to be set");
}
}
catch (BuildNotFoundException ex)
{
if (this.throwExceptionIfBuildCouldNotBeFound)
{
throw new AzureDevOpsBuildNotFoundException("Build not found", ex);
}

this.log.Warning("Could not find build");
return;
}
}

this.log.Verbose(
"Build information:\n Id: {0}\n BuildNumber: {1}",
this.build.Id,
this.build.BuildNumber);
}

/// <summary>
/// Gets a value indicating whether a build has been successfully loaded.
/// </summary>
public bool HasBuildLoaded => this.build != null;

/// <summary>
/// Gets the URL for accessing the web portal of the Azure DevOps collection.
/// </summary>
public Uri CollectionUrl { get; }

/// <summary>
/// Gets the id of the Azure DevOps project.
/// Returns <see cref="Guid.Empty"/> if no build could be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.
/// </summary>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
public Guid ProjectId
{
get
{
if (!this.ValidateBuild())
{
return Guid.Empty;
}

return this.build.Project.Id;
}
}

/// <summary>
/// Gets the name of the Azure DevOps project.
/// Returns empty string if no build could be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.
/// </summary>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
public string ProjectName
{
get
{
if (!this.ValidateBuild())
{
return string.Empty;
}

return this.build.Project.Name;
}
}

/// <summary>
/// Gets the name of the Git repository.
/// Returns <see cref="string.Empty"/> if no build could be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.
/// </summary>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
public string RepositoryName
{
get
{
if (!this.ValidateBuild())
{
return string.Empty;
}

return this.build.Repository.Name;
}
}

/// <summary>
/// Gets the ID of the repository.
/// Returns <see cref="Guid.Empty"/> if no build could be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.
/// </summary>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
public Guid RepositoryId
{
get
{
if (!this.ValidateBuild())
{
return Guid.Empty;
}

return new Guid(this.build.Repository.Id);
}
}

/// <summary>
/// Gets the ID of the build.
/// Returns 0 if no build could be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>false</c>.
/// </summary>
/// <exception cref="AzureDevOpsBuildNotFoundException">If build could not be found and
/// <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/> is set to <c>true</c>.</exception>
public int BuildId
{
get
{
if (!this.ValidateBuild())
{
return 0;
}

return this.build.Id;
}
}

/// <summary>
/// Validates if a build could be found.
/// Depending on <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/>
/// the build instance can be null for subsequent calls.
/// </summary>
/// <returns>True if a valid build instance exists.</returns>
/// <exception cref="AzureDevOpsBuildNotFoundException">If <see cref="AzureDevOpsBuildSettings.ThrowExceptionIfBuildCouldNotBeFound"/>
/// is set to <c>true</c> and no build could be found.</exception>
private bool ValidateBuild()
{
if (this.HasBuildLoaded)
{
return true;
}

if (this.throwExceptionIfBuildCouldNotBeFound)
{
throw new AzureDevOpsBuildNotFoundException("Build not found");
}

this.log.Verbose("Skipping, since no build instance could be found.");
return false;
}
}
}
62 changes: 62 additions & 0 deletions src/Cake.AzureDevOps/Build/AzureDevOpsBuildNotFoundException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Cake.AzureDevOps.Build
{
using System;
using System.Runtime.Serialization;

/// <summary>
/// Represents an error if a build was not found.
/// </summary>
[Serializable]
public class AzureDevOpsBuildNotFoundException : AzureDevOpsException
{
/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuildNotFoundException"/> class.
/// </summary>
public AzureDevOpsBuildNotFoundException()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuildNotFoundException"/> class.
/// </summary>
/// <param name="repositoryId">ID of the repository where the pull request was searched.</param>
/// <param name="pullRequestId">ID of the pull request which could not be found.</param>
public AzureDevOpsBuildNotFoundException(Guid repositoryId, int pullRequestId)
: this("Pull request with ID " + pullRequestId + " not found in repository with GUID " + repositoryId)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuildNotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error. </param>
public AzureDevOpsBuildNotFoundException(string message)
: base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuildNotFoundException"/> class with a specified error message
/// and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null
/// reference if no inner exception is specified.</param>
public AzureDevOpsBuildNotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureDevOpsBuildNotFoundException"/> class with serialized data.
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about
/// the exception being thrown.</param>
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about
/// the source or destination. </param>
protected AzureDevOpsBuildNotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
Loading