diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index bb1d61f..503efce 100644
--- a/.nuke/build.schema.json
+++ b/.nuke/build.schema.json
@@ -18,6 +18,10 @@
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
+ "Description": {
+ "type": "string",
+ "description": "Description of the pull request"
+ },
"GitHubToken": {
"type": "string",
"description": "Token used to create a new release in GitHub",
@@ -101,6 +105,7 @@
"Compile",
"Feature",
"Hotfix",
+ "ManageSecrets",
"Pack",
"Publish",
"Release",
@@ -125,6 +130,7 @@
"Compile",
"Feature",
"Hotfix",
+ "ManageSecrets",
"Pack",
"Publish",
"Release",
@@ -132,6 +138,15 @@
]
}
},
+ "Title": {
+ "type": "string",
+ "description": "Title that will be used when creating a PR"
+ },
+ "Token": {
+ "type": "string",
+ "description": "Token used to create a pull request",
+ "default": "Secrets must be entered via 'nuke :secrets [profile]'"
+ },
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
index c2f51c7..f67923a 100644
--- a/.nuke/parameters.json
+++ b/.nuke/parameters.json
@@ -1,4 +1,5 @@
{
"$schema": "./build.schema.json",
- "Solution": "Candoumbe.Pipelines.sln"
+ "Solution": "Candoumbe.Pipelines.sln",
+ "NoLogo": true
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 407480c..35bd085 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,14 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Breaking changes
- Renamed `IBenchmarks` to `IBenchmark`
- Renamed `IMutationTests` to `IMutationTest`
+- Made `IGitFlow.FinishFeature` async
+- Made `IGitFlow.FinishReleaseOrHotfix` async
+- Made `IGitFlow.FinishColdfix` async
+- Moved `GitHubPublishConfiguration` to `Candoumbe.Pipelines.Components.GitHub` namespace
+- Moved `ICreateGitHubRelease` to `Candoumbe.Pipelines.Components.GitHub` namespace
### New features
- Added execution of `IPublish.Publish` target on `integration` workflow
-- Added `IHaveReport` interface that can be used by pipelines that output reports of any kind (code coverage, performance tests, ...)
+- Added `IHaveReport` component that can be used by pipelines that output reports of any kind (code coverage, performance tests, ...)
- Added `IUnitTest.UnitTestsResultsDirectory` which defines where to output unit test result files
- Added `IMutationTest.MutationTestResultsDirectory` which defines where to output mutation test result files
- Added `IBenchmark.BenchmarkTestResultsDirectory` which defines where to output benchmarks test result files
+- Added `IPullRequest` component which extends `IGitFlow` and create pull requests instead or merging back to `develop` (respectiveley `main`) when finishing a feature / coldfix (resp. release / hotfix) branch.
+- Added `IHaveGitHubRepository` which extends `IHaveGitRepository` and specific properties related to GitHub repositories.
### Fixes
diff --git a/README.md b/README.md
index 02c54a7..40b8edd 100644
--- a/README.md
+++ b/README.md
@@ -5,4 +5,11 @@
[![GitHub raw issues](https://img.shields.io/github/issues-raw/candoumbe/pipelines)](https://github.com/candoumbe/pipelines/issues)
[![Nuget](https://img.shields.io/nuget/vpre/candoumbe.pipelines)](https://nuget.org/packages/candoumbe.pipelines)
-A Stater development kit to script your CI/CD using [Nuke](https://nuke.build).
\ No newline at end of file
+A Stater development kit to script your CI/CD using [Nuke].
+
+
+## Credits
+
+- [Nuke] as the engine behind the scene
+
+[Nuke]: https//nuke.build
\ No newline at end of file
diff --git a/build/Candoumbe.Pipelines.Build.csproj b/build/Candoumbe.Pipelines.Build.csproj
index 20c157f..b81deb6 100644
--- a/build/Candoumbe.Pipelines.Build.csproj
+++ b/build/Candoumbe.Pipelines.Build.csproj
@@ -12,17 +12,15 @@
-
-
+
-
@@ -39,6 +37,11 @@
+
+
+
+
+
diff --git a/build/Pipeline.cs b/build/Pipeline.cs
index 7282f39..9714a0c 100644
--- a/build/Pipeline.cs
+++ b/build/Pipeline.cs
@@ -1,6 +1,7 @@
namespace Candoumbe.Pipelines.Build;
using Components;
+using Components.GitHub;
using Nuke.Common;
using Nuke.Common.CI;
@@ -15,7 +16,7 @@ namespace Candoumbe.Pipelines.Build;
[GitHubActions("integration",
GitHubActionsImage.WindowsLatest,
- AutoGenerate = true,
+ AutoGenerate = false,
OnPushBranchesIgnore = new[] { IGitFlow.MainBranchName, IGitFlow.ReleaseBranch + "/*" },
FetchDepth = 0,
InvokedTargets = new[] { nameof(ICompile.Compile), nameof(IPack.Pack), nameof(IPublish.Publish) },
@@ -35,7 +36,7 @@ namespace Candoumbe.Pipelines.Build;
})]
[GitHubActions("delivery",
GitHubActionsImage.WindowsLatest,
- AutoGenerate = true,
+ AutoGenerate = false,
OnPushBranches = new[] { IGitFlow.MainBranchName, IGitFlow.ReleaseBranch + "/*" },
FetchDepth = 0,
InvokedTargets = new[] { nameof(ICompile.Compile), nameof(IPack.Pack), nameof(IPublish.Publish) },
@@ -64,11 +65,12 @@ public class Pipeline : NukeBuild,
ICompile,
IPack,
IHaveGitVersion,
- IHaveGitRepository,
+ IHaveGitHubRepository,
IHaveArtifacts,
IPublish,
ICreateGithubRelease,
- IGitFlow
+ IPullRequest,
+ IHaveSecret
{
///
IEnumerable IClean.DirectoriesToDelete => SourceDirectory.GlobDirectories("**/bin", "**/obj");
@@ -76,8 +78,8 @@ public class Pipeline : NukeBuild,
///
IEnumerable IClean.DirectoriesToEnsureExistance => new[]
{
- From().OutputDirectory,
- From().ArtifactsDirectory,
+ this.Get().OutputDirectory,
+ this.Get().ArtifactsDirectory,
};
[CI]
@@ -93,7 +95,6 @@ public class Pipeline : NukeBuild,
///
public AbsolutePath SourceDirectory => RootDirectory / "src";
-
///
/// Token used to interact with GitHub API
///
@@ -121,12 +122,9 @@ public class Pipeline : NukeBuild,
canBeUsed: () => NugetApiKey is not null
),
new GitHubPublishConfiguration(
- githubToken: From()?.GitHubToken,
+ githubToken: this.Get()?.GitHubToken,
source: new Uri($"https://nuget.pkg.github.com/{GitHubActions?.RepositoryOwner}/index.json"),
canBeUsed: () => this is ICreateGithubRelease createRelease && createRelease.GitHubToken is not null
),
};
-
- private T From() where T : INukeBuild
- => (T)(object)this;
}
diff --git a/src/Candoumbe.Pipelines/Components/Extensions.cs b/src/Candoumbe.Pipelines/Components/Extensions.cs
index a98218e..867550e 100644
--- a/src/Candoumbe.Pipelines/Components/Extensions.cs
+++ b/src/Candoumbe.Pipelines/Components/Extensions.cs
@@ -1,4 +1,6 @@
-using System;
+using Nuke.Common;
+
+using System;
namespace Candoumbe.Pipelines.Components;
@@ -21,4 +23,8 @@ public static class Extensions
///
public static T WhenNotNull(this T settings, TObject obj, Func configurator)
=> obj is not null ? configurator.Invoke(settings, obj) : settings;
+
+
+ public static T Get(this INukeBuild nukeBuild) where T : INukeBuild
+ => (T)(object)nukeBuild;
}
\ No newline at end of file
diff --git a/src/Candoumbe.Pipelines/Components/GitHubPublishConfiguration.cs b/src/Candoumbe.Pipelines/Components/GitHub/GitHubPublishConfiguration.cs
similarity index 93%
rename from src/Candoumbe.Pipelines/Components/GitHubPublishConfiguration.cs
rename to src/Candoumbe.Pipelines/Components/GitHub/GitHubPublishConfiguration.cs
index ac938bf..6daffaf 100644
--- a/src/Candoumbe.Pipelines/Components/GitHubPublishConfiguration.cs
+++ b/src/Candoumbe.Pipelines/Components/GitHub/GitHubPublishConfiguration.cs
@@ -1,6 +1,6 @@
using System;
-namespace Candoumbe.Pipelines.Components;
+namespace Candoumbe.Pipelines.Components.GitHub;
///
/// Wraps configuration used to publish packages to nuget.org feed.
diff --git a/src/Candoumbe.Pipelines/Components/ICreateGithubRelease.cs b/src/Candoumbe.Pipelines/Components/GitHub/ICreateGithubRelease.cs
similarity index 87%
rename from src/Candoumbe.Pipelines/Components/ICreateGithubRelease.cs
rename to src/Candoumbe.Pipelines/Components/GitHub/ICreateGithubRelease.cs
index 4dcb5fa..767e748 100644
--- a/src/Candoumbe.Pipelines/Components/ICreateGithubRelease.cs
+++ b/src/Candoumbe.Pipelines/Components/GitHub/ICreateGithubRelease.cs
@@ -10,20 +10,13 @@
using static Nuke.Common.ChangeLog.ChangelogTasks;
using static Serilog.Log;
-namespace Candoumbe.Pipelines.Components;
+namespace Candoumbe.Pipelines.Components.GitHub;
///
/// Marks a pipeline that can create a GitHub release
///
-public interface ICreateGithubRelease : IHaveGitRepository, IHaveChangeLog, IHaveGitVersion
+public interface ICreateGithubRelease : IHaveGitHubRepository, IHaveChangeLog, IHaveGitVersion
{
- ///
- /// Token used to create a new GitHub release
- ///
- [Parameter("Token used to create a new release in GitHub")]
- [Secret]
- string GitHubToken => TryGetValue(() => GitHubToken) ?? GitHubActions.Instance.Token;
-
///
/// Collection of assets to add to the published release.
///
diff --git a/src/Candoumbe.Pipelines/Components/GitHub/IHaveGitHubRepository.cs b/src/Candoumbe.Pipelines/Components/GitHub/IHaveGitHubRepository.cs
new file mode 100644
index 0000000..c89f717
--- /dev/null
+++ b/src/Candoumbe.Pipelines/Components/GitHub/IHaveGitHubRepository.cs
@@ -0,0 +1,17 @@
+using Nuke.Common;
+using Nuke.Common.CI.GitHubActions;
+
+namespace Candoumbe.Pipelines.Components.GitHub;
+
+///
+/// Marks a pipeline for repositories that are stored on GitHub.
+///
+public interface IHaveGitHubRepository : IHaveGitRepository, IHaveSecret
+{
+ ///
+ /// Token used to create a new GitHub release
+ ///
+ [Parameter("Token used to create a new release in GitHub")]
+ [Secret]
+ string GitHubToken => TryGetValue(() => GitHubToken) ?? GitHubActions.Instance.Token;
+}
diff --git a/src/Candoumbe.Pipelines/Components/GitHub/IPullRequest.cs b/src/Candoumbe.Pipelines/Components/GitHub/IPullRequest.cs
new file mode 100644
index 0000000..315ab69
--- /dev/null
+++ b/src/Candoumbe.Pipelines/Components/GitHub/IPullRequest.cs
@@ -0,0 +1,152 @@
+using Nuke.Common;
+using Nuke.Common.Git;
+using Nuke.Common.Tools.GitHub;
+
+using Octokit;
+
+using System;
+using System.Threading.Tasks;
+
+using static Nuke.Common.Tools.Git.GitTasks;
+using static Nuke.Common.Tools.GitHub.GitHubTasks;
+using static Nuke.Common.Utilities.ConsoleUtility;
+using static Serilog.Log;
+
+namespace Candoumbe.Pipelines.Components.GitHub
+{
+ ///
+ /// This interface adds a target to open a pull request
+ ///
+ public interface IPullRequest : IGitFlow, IHaveGitHubRepository
+ {
+ ///
+ /// The title of the PR that will be created
+ ///
+ [Parameter("Title that will be used when creating a PR")]
+ string Title => TryGetValue(() => Title) ?? ((GitRepository.IsOnFeatureBranch(), GitRepository.IsOnReleaseBranch(), GitRepository.IsOnHotfixBranch()) switch
+ {
+ (true, _, _) => $"[FEATURE] {GitRepository.Branch.ToTitleCase()}",
+ (_, _, true) => $"[HOTFIX] {GitRepository.Branch.ToTitleCase()}",
+ _ => GitRepository.Branch.ToTitleCase()
+ }).Replace('-', ' ');
+
+ ///
+ /// Token that will be used to connect to GitHub
+ ///
+ [Parameter("Token used to create a pull request")]
+ [Secret]
+ string Token => TryGetValue(() => Token);
+
+ ///
+ /// Description of the pull request
+ ///
+ [Parameter("Description of the pull request")]
+ string Description => TryGetValue(() => Description) ?? this.Get()?.ReleaseNotes;
+
+ ///
+ /// Should the local branch be deleted after the pull request was created successfully ?
+ ///
+ public bool DeleteLocalOnSuccess => false;
+
+ ///
+ /// Defines, when set to , to open the pull request as draft.
+ ///
+ [Parameter("Indicates to open the pull request as 'draft'")]
+ public bool Draft => false;
+
+ ///
+ /// The issue ID for witch pull request will be created.
+ ///
+ [Parameter("The issue ID for witch pull request will be created.")]
+ uint? Issue => null;
+
+ ///
+ async ValueTask IGitFlow.FinishFeature()
+ {
+ // Push to the remote branch
+ GitPushToRemote();
+
+ string repositoryName = GitRepository.GetGitHubName();
+ string branchName = GitCurrentBranch();
+ string owner = GitRepository.GetGitHubOwner();
+
+ Information("Creating a pull request for {Repository}", repositoryName);
+ string title = PromptForInput("Title of the pull request :", Title);
+
+ Information("Creating {PullRequestName} for {Repository}", title, repositoryName);
+ string token = Token ?? PromptForInput("Token (leave empty to exit)", string.Empty);
+
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ Information("{SourceBranch} ==> {TargetBranch} on the behalf of {UserName}", branchName, DevelopBranch, token);
+ GitHubClient gitHubClient = new(new ProductHeaderValue(repositoryName))
+ {
+ Credentials = new Credentials(token)
+ };
+
+ NewPullRequest newPullRequest = new(title, branchName, DevelopBranch)
+ {
+ Draft = Draft,
+ Body = Description
+ };
+
+ PullRequest pullRequest = await gitHubClient.PullRequest.Create(owner, repositoryName, newPullRequest);
+
+ Information("PR {PullRequestUrl} created successfully", pullRequest.HtmlUrl);
+ DeleteLocalBranchIf(DeleteLocalOnSuccess, branchName, switchToBranchName: DevelopBranch);
+ }
+ }
+
+ private static void GitPushToRemote()
+ {
+ Git($"push origin --set-upstream {GitCurrentBranch()}");
+ }
+
+ private static void DeleteLocalBranchIf(in bool condition, in string branchName, in string switchToBranchName)
+ {
+ if (condition)
+ {
+ if (PromptForChoice("Delete branch {BranchName} ? (Y/N)", BuildChoices()) == ConsoleKey.Y)
+ {
+ Git($"switch {switchToBranchName}");
+ Git($"branch -D {branchName}");
+ }
+ }
+
+ static (ConsoleKey key, string description)[] BuildChoices() => new[]
+ {
+ (key: ConsoleKey.Y, "Delete the local branch"),
+ (key: ConsoleKey.N, "Keep the local branch"),
+ };
+ }
+
+ ///
+ async ValueTask IGitFlow.FinishReleaseOrHotfix()
+ {
+ GitPushToRemote();
+
+ string repositoryName = GitRepository.GetGitHubName();
+ string branchName = GitCurrentBranch();
+
+ Information("Creating a pull request for {Repository}", repositoryName);
+ string title = PromptForInput("Title of the pull request :", Title);
+
+ Information("Creating {PullRequestName} for {Repository}", title, repositoryName);
+ string token = Token ?? PromptForInput("Token (leave empty to exit)", string.Empty);
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ Verbose("{SourceBranch} ==> {TargetBranch}", branchName, DevelopBranch, token);
+ GitHubClient gitHubClient = new(new ProductHeaderValue(repositoryName))
+ {
+ Credentials = new Credentials(token)
+ };
+ NewPullRequest newPullRequest = new(Title, GitRepository.Branch, MainBranchName);
+
+ PullRequest pullRequest = await gitHubClient.PullRequest.Create(GitRepository.GetGitHubOwner(), repositoryName, newPullRequest);
+
+ Information("PR {PullRequestUrl} created successfully", pullRequest.HtmlUrl);
+ DeleteLocalBranchIf(DeleteLocalOnSuccess, GitCurrentBranch(), switchToBranchName: DevelopBranch);
+ }
+ }
+ }
+}
diff --git a/src/Candoumbe.Pipelines/Components/IGitFlow.cs b/src/Candoumbe.Pipelines/Components/IGitFlow.cs
index 07cbd4d..f2ebc6f 100644
--- a/src/Candoumbe.Pipelines/Components/IGitFlow.cs
+++ b/src/Candoumbe.Pipelines/Components/IGitFlow.cs
@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading.Tasks;
using static Nuke.Common.ChangeLog.ChangelogTasks;
using static Nuke.Common.Tools.Git.GitTasks;
@@ -108,7 +109,7 @@ public interface IGitFlow : IHaveGitRepository, IHaveChangeLog, IHaveGitVersion
.Description($"Starts a new feature development by creating the associated branch {FeatureBranchPrefix}/{{feature-name}} from {DevelopBranch}")
.Requires(() => IsLocalBuild)
.Requires(() => !GitRepository.IsOnFeatureBranch() || GitHasCleanWorkingCopy())
- .Executes(() =>
+ .Executes(async () =>
{
if (!GitRepository.IsOnFeatureBranch())
{
@@ -118,7 +119,7 @@ public interface IGitFlow : IHaveGitRepository, IHaveChangeLog, IHaveGitVersion
}
else
{
- FinishFeature();
+ await FinishFeature();
}
});
@@ -178,8 +179,9 @@ private void AskBranchNameAndSwitchToIt(string branchNamePrefix, string sourceBr
public Target Release => _ => _
.DependsOn(Changelog)
.Description($"Starts a new {ReleaseBranchPrefix}/{{version}} from {DevelopBranch}")
+ .Requires(() => IsLocalBuild)
.Requires(() => !GitRepository.IsOnReleaseBranch() || GitHasCleanWorkingCopy())
- .Executes(() =>
+ .Executes(async () =>
{
if (!GitRepository.IsOnReleaseBranch())
{
@@ -187,7 +189,7 @@ private void AskBranchNameAndSwitchToIt(string branchNamePrefix, string sourceBr
}
else
{
- FinishReleaseOrHotfix();
+ await FinishReleaseOrHotfix();
}
});
@@ -200,8 +202,9 @@ private void AskBranchNameAndSwitchToIt(string branchNamePrefix, string sourceBr
public Target Hotfix => _ => _
.DependsOn(Changelog)
.Description($"Starts a new hotfix branch '{HotfixBranchPrefix}/*' from {MainBranchName}")
+ .Requires(() => IsLocalBuild)
.Requires(() => !GitRepository.IsOnHotfixBranch() || GitHasCleanWorkingCopy())
- .Executes(() =>
+ .Executes(async () =>
{
(GitVersion mainBranchVersion, IReadOnlyCollection