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

Add support for GitLab #523

Merged
merged 13 commits into from
Nov 10, 2023
25 changes: 21 additions & 4 deletions src/GitReleaseManager.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
using GitReleaseManager.Core.Commands;
using GitReleaseManager.Core.Configuration;
using GitReleaseManager.Core.Helpers;
using GitReleaseManager.Core.Model;
using GitReleaseManager.Core.Options;
using GitReleaseManager.Core.Provider;
using GitReleaseManager.Core.ReleaseNotes;
using GitReleaseManager.Core.Templates;
using Microsoft.Extensions.DependencyInjection;
using NGitLab;
using Octokit;
using Serilog;

Expand Down Expand Up @@ -96,7 +98,6 @@ private static void RegisterServices(BaseSubOptions options)
.AddSingleton<IFileSystem>(fileSystem)
.AddSingleton<IReleaseNotesExporter, ReleaseNotesExporter>()
.AddSingleton<IReleaseNotesBuilder, ReleaseNotesBuilder>()
.AddSingleton<IVcsProvider, GitHubProvider>()
.AddSingleton<IVcsService, VcsService>();

if (options is BaseVcsOptions vcsOptions)
Expand All @@ -106,9 +107,7 @@ private static void RegisterServices(BaseSubOptions options)
throw new Exception("The token option is not defined");
}

var gitHubClient = new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) };
serviceCollection = serviceCollection
.AddSingleton<IGitHubClient>(gitHubClient);
RegisterVcsProvider(vcsOptions, serviceCollection);
}

serviceCollection = serviceCollection
Expand Down Expand Up @@ -197,5 +196,23 @@ private static Task<int> ExecuteCommand<TOptions>(TOptions options)

private static void LogOptions(BaseSubOptions options)
=> Log.Debug("{@Options}", options);

private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceCollection serviceCollection)
{
Log.Information("Using {Provider} as VCS Provider", vcsOptions.Provider);
if (vcsOptions.Provider == VcsProvider.GitLab)
{
serviceCollection
.AddSingleton<IGitLabClient>((_) => new GitLabClient("https://gitlab.com", vcsOptions.Token))
.AddSingleton<IVcsProvider, GitLabProvider>();
}
else
{
// default to Github
serviceCollection
.AddSingleton<IGitHubClient>((_) => new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) })
.AddSingleton<IVcsProvider, GitHubProvider>();
}
}
}
}
78 changes: 39 additions & 39 deletions src/GitReleaseManager.Core.Tests/Provider/GitHubProviderTests.cs

Large diffs are not rendered by default.

45 changes: 25 additions & 20 deletions src/GitReleaseManager.Core.Tests/VcsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class VcsServiceTests
{
private const string OWNER = "owner";
private const string REPOSITORY = "repository";
private const int MILESTONE_NUMBER = 1;
private const int MILESTONE_PUBLIC_NUMBER = 1;
private const int MILESTONE_INTERNAL_NUMBER = 123;
private const string MILESTONE_TITLE = "0.1.0";
private const string TAG_NAME = "0.1.0";
private const string RELEASE_NOTES = "Release Notes";
Expand Down Expand Up @@ -126,7 +127,7 @@ public async Task Should_Add_Assets()
await _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, _assets).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any<int>()).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any<ReleaseAsset>()).ConfigureAwait(false);
await _vcsProvider.Received(assetsCount).UploadAssetAsync(release, Arg.Any<ReleaseAssetUpload>()).ConfigureAwait(false);

_logger.DidNotReceive().Warning(Arg.Any<string>(), Arg.Any<string>());
Expand All @@ -149,7 +150,7 @@ public async Task Should_Add_Assets_With_Deleting_Existing_Assets()
await _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, _assets).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.Received(releaseAssetsCount).DeleteAssetAsync(OWNER, REPOSITORY, releaseAsset.Id).ConfigureAwait(false);
await _vcsProvider.Received(releaseAssetsCount).DeleteAssetAsync(OWNER, REPOSITORY, releaseAsset).ConfigureAwait(false);
await _vcsProvider.Received(assetsCount).UploadAssetAsync(release, Arg.Any<ReleaseAssetUpload>()).ConfigureAwait(false);

_logger.Received(releaseAssetsCount).Warning(Arg.Any<string>(), Arg.Any<string>());
Expand All @@ -172,7 +173,7 @@ public async Task Should_Throw_Exception_On_Adding_Assets_When_Asset_File_Not_Ex
ex.Message.ShouldContain(assetFilePath);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any<int>()).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any<ReleaseAsset>()).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().UploadAssetAsync(release, Arg.Any<ReleaseAssetUpload>()).ConfigureAwait(false);
}

Expand All @@ -182,7 +183,7 @@ public async Task Should_Do_Nothing_On_Missing_Assets(IList<string> assets)
await _vcsService.AddAssetsAsync(OWNER, REPOSITORY, TAG_NAME, assets).ConfigureAwait(false);

await _vcsProvider.DidNotReceive().GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any<int>()).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteAssetAsync(OWNER, REPOSITORY, Arg.Any<ReleaseAsset>()).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().UploadAssetAsync(Arg.Any<Release>(), Arg.Any<ReleaseAssetUpload>()).ConfigureAwait(false);
}

Expand All @@ -205,7 +206,7 @@ public async Task Should_Create_Labels()
_vcsProvider.GetLabelsAsync(OWNER, REPOSITORY)
.Returns(Task.FromResult((IEnumerable<Label>)labels));

_vcsProvider.DeleteLabelAsync(OWNER, REPOSITORY, Arg.Any<string>())
_vcsProvider.DeleteLabelAsync(OWNER, REPOSITORY, Arg.Any<Label>())
.Returns(Task.CompletedTask);

_vcsProvider.CreateLabelAsync(OWNER, REPOSITORY, Arg.Any<Label>())
Expand All @@ -214,7 +215,7 @@ public async Task Should_Create_Labels()
await _vcsService.CreateLabelsAsync(OWNER, REPOSITORY).ConfigureAwait(false);

await _vcsProvider.Received(1).GetLabelsAsync(OWNER, REPOSITORY).ConfigureAwait(false);
await _vcsProvider.Received(labels.Count).DeleteLabelAsync(OWNER, REPOSITORY, Arg.Any<string>()).ConfigureAwait(false);
await _vcsProvider.Received(labels.Count).DeleteLabelAsync(OWNER, REPOSITORY, Arg.Any<Label>()).ConfigureAwait(false);
await _vcsProvider.Received(_configuration.Labels.Count).CreateLabelAsync(OWNER, REPOSITORY, Arg.Any<Label>()).ConfigureAwait(false);

_logger.Received(1).Verbose(Arg.Any<string>(), OWNER, REPOSITORY);
Expand All @@ -235,61 +236,65 @@ public async Task Should_Log_An_Warning_When_Labels_Not_Configured()
[Test]
public async Task Should_Close_Milestone()
{
var milestone = new Milestone { Number = MILESTONE_NUMBER };
var milestone = new Milestone { PublicNumber = MILESTONE_PUBLIC_NUMBER, InternalNumber = MILESTONE_INTERNAL_NUMBER };

_vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open)
.Returns(Task.FromResult(milestone));

_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Closed)
_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone, ItemState.Closed)
.Returns(Task.CompletedTask);

await _vcsService.CloseMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Closed).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone, ItemState.Closed).ConfigureAwait(false);
}

[Test]
public async Task Should_Log_An_Warning_On_Closing_When_Milestone_Cannot_Be_Found()
{
var milestone = new Milestone { PublicNumber = MILESTONE_PUBLIC_NUMBER, InternalNumber = MILESTONE_INTERNAL_NUMBER };

_vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open)
.Returns(Task.FromException<Milestone>(_notFoundException));

await _vcsService.CloseMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Open).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, ItemState.Closed).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, milestone, ItemState.Closed).ConfigureAwait(false);
_logger.Received(1).Warning(UNABLE_TO_FOUND_MILESTONE_MESSAGE, "open", MILESTONE_TITLE, OWNER, REPOSITORY);
}

[Test]
public async Task Should_Open_Milestone()
{
var milestone = new Milestone { Number = MILESTONE_NUMBER };
var milestone = new Milestone { PublicNumber = MILESTONE_PUBLIC_NUMBER, InternalNumber = MILESTONE_INTERNAL_NUMBER };

_vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed)
.Returns(Task.FromResult(milestone));

_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Open)
_vcsProvider.SetMilestoneStateAsync(OWNER, REPOSITORY, milestone, ItemState.Open)
.Returns(Task.CompletedTask);

await _vcsService.OpenMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone.Number, ItemState.Open).ConfigureAwait(false);
await _vcsProvider.Received(1).SetMilestoneStateAsync(OWNER, REPOSITORY, milestone, ItemState.Open).ConfigureAwait(false);
_logger.Received(2).Verbose(Arg.Any<string>(), MILESTONE_TITLE, OWNER, REPOSITORY);
}

[Test]
public async Task Should_Log_An_Warning_On_Opening_When_Milestone_Cannot_Be_Found()
{
var milestone = new Milestone { PublicNumber = MILESTONE_PUBLIC_NUMBER, InternalNumber = MILESTONE_INTERNAL_NUMBER };

_vcsProvider.GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed)
.Returns(Task.FromException<Milestone>(_notFoundException));

await _vcsService.OpenMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false);

await _vcsProvider.Received(1).GetMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ItemStateFilter.Closed).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, MILESTONE_NUMBER, ItemState.Open).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().SetMilestoneStateAsync(OWNER, REPOSITORY, milestone, ItemState.Open).ConfigureAwait(false);
_logger.Received(1).Warning(UNABLE_TO_FOUND_MILESTONE_MESSAGE, "closed", MILESTONE_TITLE, OWNER, REPOSITORY);
}

Expand Down Expand Up @@ -556,13 +561,13 @@ public async Task Should_Delete_Draft_Release()
_vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME)
.Returns(Task.FromResult(release));

_vcsProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release.Id)
_vcsProvider.DeleteReleaseAsync(OWNER, REPOSITORY, release)
.Returns(Task.CompletedTask);

await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release.Id).ConfigureAwait(false);
await _vcsProvider.Received(1).DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false);
}

[Test]
Expand All @@ -576,7 +581,7 @@ public async Task Should_Not_Delete_Published_Release()
await _vcsService.DiscardReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release.Id).ConfigureAwait(false);
await _vcsProvider.DidNotReceive().DeleteReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false);
_logger.Received(1).Warning(Arg.Any<string>(), TAG_NAME);
}

Expand All @@ -601,13 +606,13 @@ public async Task Should_Publish_Release()
_vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME)
.Returns(Task.FromResult(release));

_vcsProvider.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release.Id)
_vcsProvider.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release)
.Returns(Task.CompletedTask);

await _vcsService.PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);

await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, TAG_NAME).ConfigureAwait(false);
await _vcsProvider.Received(1).PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release.Id).ConfigureAwait(false);
await _vcsProvider.Received(1).PublishReleaseAsync(OWNER, REPOSITORY, TAG_NAME, release).ConfigureAwait(false);
_logger.Received(1).Verbose(Arg.Any<string>(), TAG_NAME, OWNER, REPOSITORY);
_logger.Received(1).Debug(Arg.Any<string>(), Arg.Any<Release>());
}
Expand Down
8 changes: 8 additions & 0 deletions src/GitReleaseManager.Core/Commands/AddAssetsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public AddAssetsCommand(IVcsService vcsService, ILogger logger)

public async Task<int> ExecuteAsync(AddAssetSubOptions options)
{
var vcsOptions = options as BaseVcsOptions;

if (vcsOptions?.Provider == Model.VcsProvider.GitLab)
{
_logger.Error("The 'addasset' command is currently not supported when targeting GitLab.");
return 1;
}

_logger.Information("Uploading assets");
await _vcsService.AddAssetsAsync(options.RepositoryOwner, options.RepositoryName, options.TagName, options.AssetPaths).ConfigureAwait(false);

Expand Down
10 changes: 9 additions & 1 deletion src/GitReleaseManager.Core/Commands/ExportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ public ExportCommand(IVcsService vcsService, ILogger logger)

public async Task<int> ExecuteAsync(ExportSubOptions options)
{
_logger.Information("Exporting release {TagName}", options.TagName);
if (string.IsNullOrWhiteSpace(options.TagName))
{
_logger.Information("Exporting all releases.");
}
else
{
_logger.Information("Exporting release {TagName}.", options.TagName);
}

var releasesContent = await _vcsService.ExportReleasesAsync(options.RepositoryOwner, options.RepositoryName, options.TagName, options.SkipPrereleases).ConfigureAwait(false);

using (var sw = new StreamWriter(File.Open(options.FileOutputPath, FileMode.OpenOrCreate)))
Expand Down
8 changes: 8 additions & 0 deletions src/GitReleaseManager.Core/Commands/LabelCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public LabelCommand(IVcsService vcsService, ILogger logger)

public async Task<int> ExecuteAsync(LabelSubOptions options)
{
var vcsOptions = options as BaseVcsOptions;

if (vcsOptions?.Provider == Model.VcsProvider.GitLab)
{
_logger.Error("The label command is currently not supported when targeting GitLab.");
return 1;
}

_logger.Information("Creating standard labels");
await _vcsService.CreateLabelsAsync(options.RepositoryOwner, options.RepositoryName).ConfigureAwait(false);

Expand Down
2 changes: 1 addition & 1 deletion src/GitReleaseManager.Core/Configuration/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class Config

- [GitHub release](https://github.com/{owner}/{repository}/releases/tag/{milestone})

Your **[GitReleaseManager](https://github.com/GitTools/GitReleaseManager)** bot :package::rocket:";
Your **[GitReleaseManager](https://github.com/GitTools/GitReleaseManager)** bot :package: :rocket:";

public Config()
{
Expand Down
26 changes: 24 additions & 2 deletions src/GitReleaseManager.Core/Extensions/MilestoneExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using Octokit;
using Serilog;

namespace GitReleaseManager.Core.Extensions
Expand All @@ -8,7 +7,30 @@ public static class MilestoneExtensions
{
public static readonly ILogger _logger = Log.ForContext(typeof(MilestoneExtensions));

public static Version Version(this Milestone ver)
public static Version Version(this Octokit.Milestone ver)
{
if (ver is null)
{
throw new ArgumentNullException(nameof(ver));
}

var nameWithoutPrerelease = ver.Title.Split('-')[0];
AdmiringWorm marked this conversation as resolved.
Show resolved Hide resolved
if (nameWithoutPrerelease.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
_logger.Debug("Removing version prefix from {Name}.", ver.Title);
nameWithoutPrerelease = nameWithoutPrerelease.Remove(0, 1);
}

if (!System.Version.TryParse(nameWithoutPrerelease, out Version parsedVersion))
{
_logger.Warning("No valid version was found on {Title}.", ver.Title);
return new Version(0, 0);
}

return parsedVersion;
}

public static Version Version(this NGitLab.Models.Milestone ver)
{
if (ver is null)
{
Expand Down
1 change: 1 addition & 0 deletions src/GitReleaseManager.Core/GitReleaseManager.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NGitLab" Version="6.39.0" />
<PackageReference Include="Octokit" Version="7.1.0" />
<PackageReference Include="Scriban" Version="5.7.0" />
<PackageReference Include="seriloganalyzer" Version="0.15.0" />
Expand Down
Loading
Loading