From 3b361e3294bde9e54a82f9e3d007a5f8676457d5 Mon Sep 17 00:00:00 2001 From: Andrey Akinshin Date: Sat, 8 Jul 2023 01:59:03 +0200 Subject: [PATCH] Add Release build task --- build/BenchmarkDotNet.Build/BuildContext.cs | 49 ++--- .../BenchmarkDotNet.Build/ChangeLogBuilder.cs | 4 +- .../Meta/GitHubCredentials.cs | 9 + build/BenchmarkDotNet.Build/Meta/Repo.cs | 3 + build/BenchmarkDotNet.Build/Program.cs | 11 ++ .../Runners/BuildRunner.cs | 13 ++ .../Runners/DocumentationRunner.cs | 37 ++-- .../Runners/GitRunner.cs | 106 ++++++++++ .../Runners/ReleaseRunner.cs | 182 ++++++++++++++++++ 9 files changed, 367 insertions(+), 47 deletions(-) create mode 100644 build/BenchmarkDotNet.Build/Runners/GitRunner.cs create mode 100644 build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs diff --git a/build/BenchmarkDotNet.Build/BuildContext.cs b/build/BenchmarkDotNet.Build/BuildContext.cs index 4b98174137..d7b12205f9 100644 --- a/build/BenchmarkDotNet.Build/BuildContext.cs +++ b/build/BenchmarkDotNet.Build/BuildContext.cs @@ -26,6 +26,8 @@ public class BuildContext : FrostingContext public DotNetVerbosity BuildVerbosity { get; set; } = DotNetVerbosity.Minimal; public int Depth { get; set; } public bool VersionStable { get; } + public string NextVersion { get; } + public bool PushMode { get; } public DirectoryPath RootDirectory { get; } public DirectoryPath BuildDirectory { get; } @@ -34,6 +36,8 @@ public class BuildContext : FrostingContext public FilePath SolutionFile { get; } public FilePath TemplatesTestsProjectFile { get; } public FilePathCollection AllPackableSrcProjects { get; } + public FilePath VersionsFile { get; } + public FilePath CommonPropsFile { get; } public DotNetMSBuildSettings MsBuildSettingsRestore { get; } public DotNetMSBuildSettings MsBuildSettingsBuild { get; } @@ -45,9 +49,11 @@ public class BuildContext : FrostingContext public VersionHistory VersionHistory { get; } + public GitRunner GitRunner { get; } public UnitTestRunner UnitTestRunner { get; } public DocumentationRunner DocumentationRunner { get; } public BuildRunner BuildRunner { get; } + public ReleaseRunner ReleaseRunner { get; } public BuildContext(ICakeContext context) : base(context) @@ -64,6 +70,9 @@ public BuildContext(ICakeContext context) AllPackableSrcProjects = new FilePathCollection(context.GetFiles(RootDirectory.FullPath + "/src/**/*.csproj") .Where(p => !p.FullPath.Contains("Disassembler"))); + VersionsFile = BuildDirectory.CombineWithFilePath("versions.txt"); + CommonPropsFile = BuildDirectory.CombineWithFilePath("common.props"); + MsBuildSettingsRestore = new DotNetMSBuildSettings(); MsBuildSettingsBuild = new DotNetMSBuildSettings(); MsBuildSettingsPack = new DotNetMSBuildSettings(); @@ -78,6 +87,8 @@ public BuildContext(ICakeContext context) Depth = -1; VersionStable = false; + NextVersion = ""; + PushMode = false; if (context.Arguments.HasArgument("msbuild")) { var msBuildParameters = context.Arguments.GetArguments().First(it => it.Key == "msbuild").Value; @@ -107,6 +118,12 @@ public BuildContext(ICakeContext context) if (name.Equals("VersionStable", StringComparison.OrdinalIgnoreCase) && value != "") VersionStable = true; + + if (name.Equals("NextVersion", StringComparison.OrdinalIgnoreCase) && value != "") + NextVersion = value; + + if (name.Equals("PushMode", StringComparison.OrdinalIgnoreCase) && value != "") + PushMode = true; } } } @@ -129,11 +146,13 @@ public BuildContext(ICakeContext context) nuGetPackageNames.Sort(); NuGetPackageNames = nuGetPackageNames; - VersionHistory = new VersionHistory(this, BuildDirectory.CombineWithFilePath("versions.txt")); + VersionHistory = new VersionHistory(this, VersionsFile); + GitRunner = new GitRunner(this); UnitTestRunner = new UnitTestRunner(this); DocumentationRunner = new DocumentationRunner(this); BuildRunner = new BuildRunner(this); + ReleaseRunner = new ReleaseRunner(this); } public void GenerateFile(FilePath filePath, StringBuilder content) @@ -160,32 +179,4 @@ public void GenerateFile(FilePath filePath, string content) } } - public void Clone(DirectoryPath path, string repoUrl, string branchName) - { - this.Information($"[GitClone]"); - this.Information($" Repo: {repoUrl}"); - this.Information($" Branch: {branchName}"); - this.Information($" Path: {path}"); - var settings = new GitCloneSettings { Checkout = true, BranchName = branchName }; - try - { - this.GitClone(repoUrl, path, settings); - this.Information(" Success"); - } - catch (Exception e) - { - this.Error($" Failed to clone via API (Exception: {e.GetType().Name})'"); - try - { - var gitArgs = $"clone -b {branchName} {repoUrl} {path}"; - this.Information($" Trying to clone manually using 'git {gitArgs}'"); - this.StartProcess("git", gitArgs); - this.Information(" Success"); - } - catch (Exception e2) - { - throw new Exception($"Failed to clone {repoUrl} to {path} (branch: '{branchName})'", e2); - } - } - } } \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/ChangeLogBuilder.cs b/build/BenchmarkDotNet.Build/ChangeLogBuilder.cs index 41bba947f0..ce628d09d6 100644 --- a/build/BenchmarkDotNet.Build/ChangeLogBuilder.cs +++ b/build/BenchmarkDotNet.Build/ChangeLogBuilder.cs @@ -60,9 +60,7 @@ private async Task Build() if (string.IsNullOrEmpty(lastCommit)) lastCommit = $"v{currentVersion}"; - var client = new GitHubClient(new ProductHeaderValue(GitHubCredentials.ProductHeader)); - var tokenAuth = new Credentials(GitHubCredentials.Token); - client.Credentials = tokenAuth; + var client = GitHubCredentials.CreateClient(); if (currentVersion == "_") { diff --git a/build/BenchmarkDotNet.Build/Meta/GitHubCredentials.cs b/build/BenchmarkDotNet.Build/Meta/GitHubCredentials.cs index dc09bce5ab..c174784417 100644 --- a/build/BenchmarkDotNet.Build/Meta/GitHubCredentials.cs +++ b/build/BenchmarkDotNet.Build/Meta/GitHubCredentials.cs @@ -1,4 +1,5 @@ using System; +using Octokit; namespace BenchmarkDotNet.Build.Meta; @@ -8,4 +9,12 @@ public static class GitHubCredentials public const string ProductHeader = "BenchmarkDotNet"; public static string? Token => Environment.GetEnvironmentVariable(TokenVariableName); + + public static GitHubClient CreateClient() + { + var client = new GitHubClient(new ProductHeaderValue(ProductHeader)); + var tokenAuth = new Credentials(Token); + client.Credentials = tokenAuth; + return client; + } } \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/Meta/Repo.cs b/build/BenchmarkDotNet.Build/Meta/Repo.cs index 4eace53e81..ec1231a9d0 100644 --- a/build/BenchmarkDotNet.Build/Meta/Repo.cs +++ b/build/BenchmarkDotNet.Build/Meta/Repo.cs @@ -6,5 +6,8 @@ public static class Repo public const string Name = "BenchmarkDotNet"; public const string HttpsUrlBase = $"https://github.com/{Owner}/{Name}"; public const string HttpsGitUrl = $"{HttpsUrlBase}.git"; + public const string ChangelogDetailsBranch = "docs-changelog-details"; + public const string DocsStableBranch = "docs-stable"; + public const string MasterBranch = "master"; } \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/Program.cs b/build/BenchmarkDotNet.Build/Program.cs index eaff56db0c..42cc824d1a 100644 --- a/build/BenchmarkDotNet.Build/Program.cs +++ b/build/BenchmarkDotNet.Build/Program.cs @@ -119,6 +119,17 @@ public class DocsBuildTask : FrostingTask public override void Run(BuildContext context) => context.DocumentationRunner.Build(); } +[TaskName("Release")] +[TaskDescription("Release new version")] +[IsDependentOn(typeof(BuildTask))] +[IsDependentOn(typeof(PackTask))] +[IsDependentOn(typeof(DocsUpdateTask))] +[IsDependentOn(typeof(DocsBuildTask))] +public class ReleaseTask : FrostingTask +{ + public override void Run(BuildContext context) => context.ReleaseRunner.Run(); +} + [TaskName("FastTests")] [TaskDescription("OBSOLETE: use 'UnitTests'")] [IsDependentOn(typeof(UnitTestsTask))] diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs index 8ef40475d0..01c490fce3 100644 --- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs +++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs @@ -6,6 +6,7 @@ using Cake.Common.Tools.DotNet.Pack; using Cake.Common.Tools.DotNet.Restore; using Cake.Core; +using Cake.Core.IO; namespace BenchmarkDotNet.Build.Runners; @@ -40,6 +41,18 @@ public void Build() }); } + public void BuildProjectSilent(FilePath projectFile) + { + context.DotNetBuild(projectFile.FullPath, new DotNetBuildSettings + { + NoRestore = false, + DiagnosticOutput = false, + MSBuildSettings = context.MsBuildSettingsBuild, + Configuration = context.BuildConfiguration, + Verbosity = DotNetVerbosity.Quiet + }); + } + public void Pack() { context.CleanDirectory(context.ArtifactsDirectory); diff --git a/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs b/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs index 5301713873..10ea30f64f 100644 --- a/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs +++ b/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs @@ -15,8 +15,8 @@ public class DocumentationRunner { private readonly BuildContext context; - private readonly DirectoryPath changelogDirectory; - private readonly DirectoryPath changelogSrcDirectory; + public DirectoryPath ChangelogDirectory { get; } + public DirectoryPath ChangelogSrcDirectory { get; } private readonly DirectoryPath changelogDetailsDirectory; private readonly DirectoryPath docsGeneratedDirectory; @@ -34,19 +34,19 @@ public DocumentationRunner(BuildContext context) this.context = context; var docsDirectory = context.RootDirectory.Combine("docs"); - changelogDirectory = docsDirectory.Combine("changelog"); - changelogSrcDirectory = docsDirectory.Combine("_changelog"); - changelogDetailsDirectory = changelogSrcDirectory.Combine("details"); + ChangelogDirectory = docsDirectory.Combine("changelog"); + ChangelogSrcDirectory = docsDirectory.Combine("_changelog"); + changelogDetailsDirectory = ChangelogSrcDirectory.Combine("details"); docsGeneratedDirectory = docsDirectory.Combine("_site"); redirectFile = docsDirectory.Combine("_redirects").CombineWithFilePath("_redirects"); docfxJsonFile = docsDirectory.CombineWithFilePath("docfx.json"); readmeFile = context.RootDirectory.CombineWithFilePath("README.md"); rootIndexFile = docsDirectory.CombineWithFilePath("index.md"); - changelogIndexFile = changelogDirectory.CombineWithFilePath("index.md"); - changelogFullFile = changelogDirectory.CombineWithFilePath("full.md"); - changelogTocFile = changelogDirectory.CombineWithFilePath("toc.yml"); - lastFooterFile = changelogSrcDirectory.Combine("footer") + changelogIndexFile = ChangelogDirectory.CombineWithFilePath("index.md"); + changelogFullFile = ChangelogDirectory.CombineWithFilePath("full.md"); + changelogTocFile = ChangelogDirectory.CombineWithFilePath("toc.yml"); + lastFooterFile = ChangelogSrcDirectory.Combine("footer") .CombineWithFilePath("v" + context.VersionHistory.CurrentVersion + ".md"); } @@ -132,6 +132,10 @@ private void GenerateIndexMd() private void GenerateChangelogToc() { var content = new StringBuilder(); + + content.AppendLine($"- name: {context.VersionHistory.CurrentVersion}"); + content.AppendLine($" href: {context.VersionHistory.CurrentVersion}.md"); + foreach (var version in context.VersionHistory.StableVersions.Reverse()) { content.AppendLine($"- name: {version}"); @@ -153,6 +157,8 @@ private void GenerateChangelogFull() content.AppendLine(""); content.AppendLine("# Full ChangeLog"); content.AppendLine(""); + content.AppendLine( + $"[!include[{context.VersionHistory.CurrentVersion}]({context.VersionHistory.CurrentVersion}.md)]"); foreach (var version in context.VersionHistory.StableVersions.Reverse()) content.AppendLine($"[!include[{version}]({version}.md)]"); @@ -168,6 +174,7 @@ private void GenerateChangelogIndex() content.AppendLine(""); content.AppendLine("# ChangeLog"); content.AppendLine(""); + content.AppendLine($"* @changelog.{context.VersionHistory.CurrentVersion}"); foreach (var version in context.VersionHistory.StableVersions.Reverse()) content.AppendLine($"* @changelog.{version}"); content.AppendLine("* @changelog.full"); @@ -178,10 +185,10 @@ private void GenerateChangelogIndex() private void DocfxChangelogGenerate(string version) { EnsureChangelogDetailsExist(); - var header = changelogSrcDirectory.Combine("header").CombineWithFilePath(version + ".md"); - var footer = changelogSrcDirectory.Combine("footer").CombineWithFilePath(version + ".md"); - var details = changelogSrcDirectory.Combine("details").CombineWithFilePath(version + ".md"); - var release = changelogDirectory.CombineWithFilePath(version + ".md"); + var header = ChangelogSrcDirectory.Combine("header").CombineWithFilePath(version + ".md"); + var footer = ChangelogSrcDirectory.Combine("footer").CombineWithFilePath(version + ".md"); + var details = ChangelogSrcDirectory.Combine("details").CombineWithFilePath(version + ".md"); + var release = ChangelogDirectory.CombineWithFilePath(version + ".md"); var content = new StringBuilder(); content.AppendLine("---"); @@ -224,7 +231,7 @@ private void EnsureChangelogDetailsExist(bool forceClean = false) new DeleteDirectorySettings { Force = true, Recursive = true }); if (!context.DirectoryExists(changelogDetailsDirectory)) - context.Clone(changelogDetailsDirectory, Repo.HttpsGitUrl, Repo.ChangelogDetailsBranch); + context.GitRunner.Clone(changelogDetailsDirectory, Repo.HttpsGitUrl, Repo.ChangelogDetailsBranch); } private void DocfxChangelogDownload(string version, string versionPrevious, string lastCommit = "") @@ -273,7 +280,7 @@ private void UpdateLastFooter() var version = context.VersionHistory.CurrentVersion; var previousVersion = context.VersionHistory.StableVersions.Last(); var date = context.VersionStable - ? DateTime.Now.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture) + ? DateTime.Now.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture) : "TBA"; var content = new StringBuilder(); diff --git a/build/BenchmarkDotNet.Build/Runners/GitRunner.cs b/build/BenchmarkDotNet.Build/Runners/GitRunner.cs new file mode 100644 index 0000000000..0fd1ae5640 --- /dev/null +++ b/build/BenchmarkDotNet.Build/Runners/GitRunner.cs @@ -0,0 +1,106 @@ +using System; +using Cake.Common; +using Cake.Common.Diagnostics; +using Cake.Core.IO; +using Cake.Git; + +namespace BenchmarkDotNet.Build.Runners; + +// Cake.Git 3.0.0 may experience the following issues on macOS: +// > Error: System.TypeInitializationException: The type initializer for 'LibGit2Sharp.Core.NativeMethods' threw an exception. +// > ---> System.DllNotFoundException: Unable to load shared library 'git2-6777db8' or one of its dependencies. In order to help diagnose loading problems, consider setting the DYLD_PRINT_LIBRARIES environment variable +// In order to workaround this problem, we provide command-line fallbacks for all the used commands. +public class GitRunner +{ + private BuildContext context; + + public GitRunner(BuildContext context) + { + this.context = context; + } + + public void Clone(DirectoryPath workDirectoryPath, string sourceUrl, string branchName) + { + context.Information($"[GitClone]"); + context.Information($" Repo: {sourceUrl}"); + context.Information($" Branch: {branchName}"); + context.Information($" Path: {workDirectoryPath}"); + + var settings = new GitCloneSettings { Checkout = true, BranchName = branchName }; + RunCommand( + () => context.GitClone(sourceUrl, workDirectoryPath, settings), + $"clone -b {branchName} {sourceUrl} {workDirectoryPath}"); + } + + public void Tag(string tagName) + { + context.Information("[GitTag]"); + context.Information($" Path: {context.RootDirectory}"); + context.Information($" TagName: {tagName}"); + + RunCommand( + () => context.GitTag(context.RootDirectory, tagName), + $"tag {tagName}"); + } + + public void BranchMove(string branchName, string target) + { + context.Information("[GitBranchMove]"); + context.Information($" Branch: {branchName}"); + context.Information($" Target: {target}"); + RunCommand($"branch -f {branchName} {target}"); + } + + public void Commit(string message) + { + context.Information("[GitCommit]"); + context.Information($" Message: {message}"); + RunCommand($"commit --all --message \"{message}\""); + } + + public void Push(string target, bool force = false) + { + context.Information("[GitPush]"); + context.Information($" Target: {target}"); + context.Information($" Force: {force}"); + if (context.PushMode) + { + var forceFlag = force ? " --force" : ""; + RunCommand($"push origin {target}{forceFlag}"); + } + else + context.Information(" Skip because PushMode is disabled"); + } + + private void RunCommand(string commandLineArgs) => RunCommand(null, commandLineArgs); + + private void RunCommand(Action? call, string commandLineArgs) + { + try + { + if (call == null) + throw new NotImplementedException(); + call(); + context.Information(" Success"); + } + catch (Exception e) + { + if (e is not NotImplementedException) + context.Information($" Failed to perform operation via API ({e.Message})"); + try + { + context.Information($" Run command in terminal: 'git {commandLineArgs}'"); + context.StartProcess("git", new ProcessSettings + { + Arguments = commandLineArgs, + WorkingDirectory = context.RootDirectory + }); + context.Information(" Success"); + } + catch (Exception e2) + { + throw new Exception($"Failed to run 'git ${commandLineArgs}'", e2); + } + } + } +} \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs b/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs new file mode 100644 index 0000000000..44a2377d0b --- /dev/null +++ b/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs @@ -0,0 +1,182 @@ +using System; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using BenchmarkDotNet.Build.Meta; +using Cake.Common.Diagnostics; +using Cake.Common.IO; +using Cake.Common.Tools.DotNet; +using Cake.Common.Tools.DotNet.NuGet.Push; +using Cake.FileHelpers; +using Octokit; + +namespace BenchmarkDotNet.Build.Runners; + +public class ReleaseRunner +{ + private readonly BuildContext context; + + public ReleaseRunner(BuildContext context) + { + this.context = context; + } + + public void Run() + { + var nextVersion = context.NextVersion; + var currentVersion = context.VersionHistory.CurrentVersion; + var isStable = context.VersionStable; + var tag = "v" + currentVersion; + + if (string.IsNullOrEmpty(nextVersion)) + throw new Exception("NextVersion is not specified"); + if (!isStable) + throw new Exception("VersionStable is not specified"); + if (string.IsNullOrEmpty(GitHubCredentials.Token)) + throw new Exception($"Environment variable '{GitHubCredentials.TokenVariableName}' is not specified!"); + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("NUGET_TOKEN"))) + throw new Exception($"Environment variable 'NUGET_TOKEN' is not specified!"); + + context.GitRunner.Tag(tag); + context.GitRunner.BranchMove(Repo.DocsStableBranch, "HEAD"); + + // Upgrade current version and commit changes + UpdateVersionsTxt(); + UpdateCommonProps(); + context.Information($"Building {context.TemplatesTestsProjectFile}"); + context.BuildRunner.BuildProjectSilent(context.TemplatesTestsProjectFile); + context.GitRunner.Commit($"Set next BenchmarkDotNet version: {nextVersion}"); + + UpdateMilestones().Wait(); + + context.GitRunner.Push(Repo.MasterBranch); + context.GitRunner.Push(Repo.DocsStableBranch, true); + context.GitRunner.Push(tag); + + PushNupkg(); + + PublishGitHubRelease().Wait(); + } + + private void UpdateVersionsTxt() + { + var content = context.FileReadText(context.VersionsFile).Trim(); + context.GenerateFile(context.VersionsFile, $"{content}\n{context.NextVersion}"); + } + + private void UpdateCommonProps() + { + var regex = new Regex(@"([\d\.]+)"); + + var content = context.FileReadText(context.CommonPropsFile); + var match = regex.Match(content); + if (!match.Success) + throw new Exception($"Failed to find VersionPrefix definition in {context.CommonPropsFile}"); + + var oldVersion = match.Groups[1].Value; + context.GenerateFile(context.CommonPropsFile, content.Replace(oldVersion, context.NextVersion)); + } + + private async Task UpdateMilestones() + { + var currentVersion = context.VersionHistory.CurrentVersion; + var nextVersion = context.NextVersion; + + var client = GitHubCredentials.CreateClient(); + var allMilestones = await client.Issue.Milestone.GetAllForRepository(Repo.Owner, Repo.Name); + var currentMilestone = allMilestones.First(milestone => milestone.Title == $"v{currentVersion}"); + + context.Information($"[GitHub] Close milestone v{currentVersion}"); + if (context.PushMode) + { + await client.Issue.Milestone.Update(Repo.Owner, Repo.Name, currentMilestone.Number, + new MilestoneUpdate { State = ItemState.Closed, DueOn = DateTimeOffset.Now }); + } + else + { + context.Information(" Skip because PushMode is disabled"); + } + + context.Information($"[GitHub] Create milestone v{nextVersion}"); + if (context.PushMode) + { + await client.Issue.Milestone.Create(Repo.Owner, Repo.Name, new NewMilestone($"v{nextVersion}")); + } + else + { + context.Information(" Skip because PushMode is disabled"); + } + } + + private void PushNupkg() + { + var nuGetToken = Environment.GetEnvironmentVariable("NUGET_TOKEN"); + + var files = context + .GetFiles(context.ArtifactsDirectory.CombineWithFilePath("*").FullPath) + .OrderBy(file => file.FullPath); + var settings = new DotNetNuGetPushSettings + { + ApiKey = nuGetToken, + SymbolApiKey = nuGetToken + }; + + foreach (var file in files) + { + context.Information($"Push: {file}"); + if (context.PushMode) + context.DotNetNuGetPush(file, settings); + else + context.Information(" Skip because PushMode is disabled"); + } + } + + private async Task PublishGitHubRelease() + { + var version = context.VersionHistory.CurrentVersion; + var tag = $"v{version}"; + var notesFile = context.DocumentationRunner + .ChangelogSrcDirectory + .Combine("header") + .CombineWithFilePath($"{tag}.md"); + var notes = $"Full changelog: https://benchmarkdotnet.org/changelog/{tag}.html\n\n" + + PreprocessMarkdown(context.FileReadText(notesFile)); + + context.Information($"[GitHub] Creating release '{version}'"); + var client = GitHubCredentials.CreateClient(); + if (context.PushMode) + { + await client.Repository.Release.Create(Repo.Owner, Repo.Name, new NewRelease(tag) + { + Name = version, + Draft = false, + Prerelease = false, + GenerateReleaseNotes = false, + Body = notes + }); + context.Information(" Success"); + } + else + { + context.Information(" Skip because PushMode is disabled"); + } + } + + private static string PreprocessMarkdown(string content) + { + var lines = content.Split("\n"); + var newContent = new StringBuilder(); + for (var i = 0; i < lines.Length; i++) + { + newContent.Append(lines[i]); + if (i == lines.Length - 1) + continue; + if (!lines[i].EndsWith(" ") && lines[i + 1].StartsWith(" ")) + continue; + newContent.Append("\n"); + } + + return newContent.ToString(); + } +} \ No newline at end of file