diff --git a/build.cake b/build.cake index 5d9ed8a..91a33c6 100644 --- a/build.cake +++ b/build.cake @@ -1,354 +1,3 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#load "./build/BuildData.cake" -#load "./build/Changelog.cake" -#load "./build/dotnet.cake" -#load "./build/environment.cake" -#load "./build/fail.cake" -#load "./build/filesystem.cake" -#load "./build/git.cake" -#load "./build/github.cake" -#load "./build/json.cake" -#load "./build/nbgv.cake" -#load "./build/options.cake" -#load "./build/process.cake" -#load "./build/public-api.cake" -#load "./build/setup-teardown.cake" -#load "./build/utilities.cake" -#load "./build/versioning.cake" -#load "./build/workspace.cake" - -#nullable enable - -using System; -using System.Text; - -using SysDirectory = System.IO.Directory; -using SysFile = System.IO.File; -using SysPath = System.IO.Path; - -// ============================================================================================= -// TASKS -// ============================================================================================= - -Task("Default") - .Description("Default task - Do nothing (but log build configuration data)") - .Does(context => { - context.Information("The default task does nothing. This is intentional."); - context.Information("Use `dotnet cake --description` to see the list of available tasks."); - }); - -Task("CleanAll") - .Description("Delete all output directories, VS data, R# caches") - .Does((context, data) => context.CleanAll(data)); - -Task("LocalCleanAll") - .Description("Like CleanAll, but only runs on a local machine") - .WithCriteria(data => data.CIPlatform is CIPlatform.None) - .Does((context, data) => context.CleanAll(data)); - -Task("Restore") - .Description("Restores dependencies") - .IsDependentOn("LocalCleanAll") - .Does((context, data) => context.RestoreSolution(data)); - -Task("Build") - .Description("Build all projects") - .IsDependentOn("Restore") - .Does((context, data) => context.BuildSolution(data, false)); - -Task("Test") - .Description("Build all projects and run tests") - .IsDependentOn("Build") - .Does((context, data) => context.TestSolution(data, false, false, true)); - -Task("Pack") - .Description("Build all projects, run tests, and prepare build artifacts") - .IsDependentOn("Test") - .Does((context, data) => context.PackSolution(data, false, false)); - -Task("Release") - .Description("Publish a new public release (CI only)") - .Does(async (context, data) => { - - // Perform some preliminary checks - context.Ensure(data.CIPlatform is not CIPlatform.None, "The Release target cannot run on a local system."); - context.Ensure(data.IsPublicRelease, "Cannot create a release from the current branch."); - - // Perform an initial versioning consistency check. - // This is a tad more relaxed than the final check, as it takes into account that we may still increment the current version - // (for example by updating the changelog). - context.CheckVersioningConsistency( - currentVersion: data.Version, - latestVersion: data.LatestVersion, - latestStableVersion: data.LatestStableVersion, - isFinalCheck: false); - - // Compute the version spec change to apply, if any. - // This implies more checks and possibly throws, so do it as early as possible. - var versionSpecChange = context.ComputeVersionSpecChange( - currentVersion: data.Version, - latestVersion: data.LatestVersion, - latestStableVersion: data.LatestStableVersion, - requestedChange: context.GetOption("versionSpecChange", VersionSpecChange.None), - checkPublicApi: context.GetOption("checkPublicApi", true)); - - // Identify Git user for later possible push - context.GitSetUserIdentity("Buildvana", "buildvana@tenacom.it"); - - // Create the release as a draft first, so if the token has no permissions we can bail out early - var release = await context.CreateDraftReleaseAsync(data); - var dupeTagChecked = false; - var committed = false; - try - { - // Modify version if required. - if (versionSpecChange != VersionSpecChange.None) - { - var versionFile = VersionFile.Load(context); - if (versionFile.ApplyVersionSpecChange(context, versionSpecChange)) - { - versionFile.Save(); - UpdateRepo(versionFile.Path); - } - } - - // Update public API files only when releasing a stable version - if (!data.IsPrerelease) - { - var modified = context.TransferAllPublicApiToShipped().ToArray(); - if (modified.Length > 0) - { - context.Information($"{modified.Length} public API files were modified."); - UpdateRepo(modified); - } - else - { - context.Information("No public API files were modified."); - } - } - else - { - context.Information("Public API update skipped: not needed on prerelease."); - } - - // Update changelog only on non-prerelease, unless forced - var changelog = new Changelog(context, data); - var changelogUpdated = false; - if (!changelog.Exists) - { - context.Information($"Changelog update skipped: {Changelog.FileName} not found."); - } - else if (!data.IsPrerelease || context.GetOption("forceUpdateChangelog", false)) - { - if (context.GetOption("checkChangelog", true)) - { - context.Ensure( - changelog.HasUnreleasedChanges(), - "Changelog check failed: the \"Unreleased changes\" section is empty or only contains sub-section headings."); - - context.Information("Changelog check successful: the \"Unreleased changes\" section is not empty."); - } - else - { - context.Information("Changelog check skipped: option 'checkChangelog' is false."); - } - - // Update the changelog and commit the change before building. - // This ensures that the Git height is up to date when computing a version for the build artifacts. - changelog.PrepareForRelease(); - UpdateRepo(changelog.Path); - changelogUpdated = true; - } - else - { - context.Information("Changelog update skipped: not needed on prerelease."); - } - - // At this point we know what the actual published version will be. - // Time for a final consistency check. - context.CheckVersioningConsistency( - currentVersion: data.Version, - latestVersion: data.LatestVersion, - latestStableVersion: data.LatestStableVersion, - isFinalCheck: true); - - // Ensure that the release tag doesn't already exist. - // This assumes that full repo history has been checked out; - // however, that is already a prerequisite for using Nerdbank.GitVersioning. - context.Ensure(!context.GitTagExists(data.VersionStr), $"Tag {data.VersionStr} already exists in repository."); - dupeTagChecked = true; - - context.RestoreSolution(data); - context.BuildSolution(data, false); - context.TestSolution(data, false, false, false); - context.PackSolution(data, false, false); - - if (changelogUpdated) - { - // Change the new section's title in the changelog to reflect the actual version. - changelog.UpdateNewSectionTitle(); - UpdateRepo(changelog.Path); - } - else - { - context.Information("Changelog section title update skipped: changelog has not been updated."); - } - - if (committed) - { - context.Information($"Git pushing changes to {data.Remote}..."); - _ = context.Exec("git", $"push {data.Remote} HEAD"); - } - else - { - context.Information("Git push skipped: no commit to push."); - } - - // Publish NuGet packages - await context.NuGetPushAllAsync(data); - - // If this is not a prerelease and we are releasing from the main branch, - // dispatch a separate workflow to publish documentation. - // Unless, of course, there is no documentation to publish, or no workflow to do it. - FilePath docFxJsonPath = "docs/docfx.json"; - FilePath pagesDeploymentWorkflow = ".github/workflows/deploy-pages.yml"; - if (data.IsPrerelease) - { - context.Information("Documentation update skipped: not needed on prerelease."); - } - else if (data.Branch != "main") - { - context.Information($"Documentation update skipped: releasing from '{data.Branch}', not 'main'."); - } - else if (!SysFile.Exists(pagesDeploymentWorkflow.FullPath)) - { - context.Information($"Documentation update skipped: {docFxJsonPath} not present."); - } - else if (!SysFile.Exists(pagesDeploymentWorkflow.FullPath)) - { - context.Warning($"Documentation update skipped: there is no documentation workflow."); - } - else - { - await context.DispatchWorkflow(data, SysPath.GetFileName(pagesDeploymentWorkflow.FullPath), "main"); - } - - // Read release asset lists and upload assets - var assets = await GetReleaseAssetsAsync().ConfigureAwait(false); - var assetCount = assets.Count; - if (assetCount > 0) - { - var i = 0; - foreach (var asset in assets) - { - i++; - context.Information($"Uploading asset {i} of {assetCount}: {SysPath.GetFileName(asset.Path)} ({asset.Description})..."); - await context.UploadReleaseAssetAsync(data, release, asset.Path, asset.MimeType, asset.Description).ConfigureAwait(false); - } - } - else - { - context.Information("Asset upload skipped: no release assets defined."); - } - - // Last but not least, publish the release. - await context.PublishReleaseAsync(data, release); - - // Set outputs for subsequent steps in GitHub Actions - if (data.CIPlatform is CIPlatform.GitHub) - { - context.SetActionsStepOutput("version", data.VersionStr); - } - } - catch (Exception e) - { - context.Error(e is CakeException ? e.Message : $"{e.GetType().Name}: {e.Message}"); - await context.DeleteReleaseAsync(data, release, dupeTagChecked ? data.VersionStr : null); - throw; - } - - void UpdateRepo(params FilePath[] files) - { - foreach (var path in files) - { - context.Verbose($"Git adding {path}..."); - _ = context.Exec( - "git", - new ProcessArgumentBuilder() - .Append("add") - .AppendQuoted(path.FullPath)); - } - - context.Information(committed ? "Amending commit..." : "Committing changed files..."); - var arguments = new ProcessArgumentBuilder().Append("commit"); - if (committed) - { - arguments = arguments.Append("--amend"); - } - - arguments = arguments.Append("-m").AppendQuoted("Prepare release [skip ci]"); - _ = context.Exec("git", arguments); - - // The commit changed the Git height, so update build data - // and amend the commit adding the right version. - // Amending a commit does not further change the Git height. - data.Update(context); - _ = context.Exec( - "git", - new ProcessArgumentBuilder() - .Append("commit") - .Append("--amend") - .Append("-m") - .AppendQuoted($"Prepare release {data.VersionStr} [skip ci]")); - - committed = true; - } - - async Task> GetReleaseAssetsAsync() - { - const string assetListMask = "*.assets.txt"; - - var result = new List<(string Path, string MimeType, string Description)>(); - if (!SysDirectory.EnumerateFiles(data.ArtifactsPath.FullPath, assetListMask).Any()) - { - context.Information("Skipping asset upload: no release asset lists."); - return result; - } - - context.Information("Reading release asset lists..."); - var assetLists = SysPath.Combine(data.ArtifactsPath.FullPath, assetListMask); - foreach (var path in context.GetFiles(assetLists).Select(x => x.FullPath)) - { - context.Verbose("Reading release asset list {path}..."); - var i = 0; - await foreach (var line in SysFile.ReadLinesAsync(path)) - { - i++; - var parts = line.Split('\t'); - if (parts.Length != 3) - { - context.Warning($"Release asset list {path}, line #{i}: invalid line '{line}'"); - continue; - } - - if (!SysFile.Exists(parts[0])) - { - context.Warning($"Release asset list {path}, line #{i}: asset not found '{parts[0]}'"); - continue; - } - - result.Add((parts[0], parts[1], parts[2])); - } - } - - return result; - } - }); - -// ============================================================================================= -// EXECUTION -// ============================================================================================= +#load nuget:?package=Buildvana.Cake&version=1.0.4-preview RunTarget(Argument("target", "Default")); diff --git a/build/BuildData.cake b/build/BuildData.cake deleted file mode 100644 index 5778d36..0000000 --- a/build/BuildData.cake +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -using NuGet.Versioning; - -// --------------------------------------------------------------------------------------------- -// CIPlatform: Continuous Integration platform under which the script runs -// --------------------------------------------------------------------------------------------- - -enum CIPlatform -{ - None, - GitHub, - GitLab, - Unsupported, -} - -// --------------------------------------------------------------------------------------------- -// BuildData: a record to hold build configuration data -// --------------------------------------------------------------------------------------------- - -/* - * Summary : Holds configuration data for the build. - */ -sealed class BuildData -{ - /* - * Summary : Initializes a new instance of the BuildData class. - * Params : context - The Cake context. - */ - public BuildData(ICakeContext context) - { - var ciPlatform = context.EnvironmentVariable("GITHUB_ACTIONS", false) ? CIPlatform.GitHub - : context.EnvironmentVariable("GITLAB_CI", false) ? CIPlatform.GitLab - : context.EnvironmentVariable("CI", false) - || context.EnvironmentVariable("CONTINUOUS_INTEGRATION", false) - || context.EnvironmentVariable("TF_BUILD", false) - || context.EnvironmentVariable("TRAVIS", false) - || context.EnvironmentVariable("APPVEYOR", false) - || context.EnvironmentVariable("CIRCLECI", false) - || context.HasEnvironmentVariable("TEAMCITY_VERSION") - || context.HasEnvironmentVariable("JENKINS_URL") - ? CIPlatform.Unsupported : CIPlatform.None; - - context.Ensure(ciPlatform is CIPlatform.None or CIPlatform.GitHub or CIPlatform.GitLab, 255, "Running under an unsupported CI"); - context.Ensure(context.TryGetRepositoryInfo(out var repository), 255, "Cannot determine repository owner and name."); - var solutionPath = context.GetFiles("*.sln").FirstOrDefault() ?? context.Fail(255, "Cannot find a solution file."); - var solution = context.ParseSolution(solutionPath); - var configuration = context.Argument("configuration", "Release"); - var artifactsPath = new DirectoryPath("artifacts").Combine(configuration); - var testResultsPath = new DirectoryPath("TestResults"); - var (versionStr, @ref, isPublicRelease, isPrerelease) = context.GetVersionInformation(); - var version = SemanticVersion.Parse(versionStr); - var branch = context.GetCurrentGitBranch(); - var msBuildSettings = new DotNetMSBuildSettings { - MaxCpuCount = 1, - ContinuousIntegrationBuild = ciPlatform is not CIPlatform.None, - NoLogo = true, - }; - - (LatestVersion, LatestStableVersion) = context.GitGetLatestVersions(); - - CIPlatform = ciPlatform; - RepositoryHostUrl = repository.HostUrl; - RepositoryOwner = repository.Owner; - RepositoryName = repository.Name; - Remote = repository.Remote; - Ref = @ref; - Branch = branch; - ArtifactsPath = artifactsPath; - TestResultsPath = testResultsPath; - SolutionPath = solutionPath; - Solution = solution; - Configuration = configuration; - VersionStr = versionStr; - Version = version; - IsPublicRelease = isPublicRelease; - IsPrerelease = isPrerelease; - MSBuildSettings = msBuildSettings; - - context.Information("Build configuration data:"); - context.Information($"CI platform : {CIPlatform}"); - context.Information($"Repository : {RepositoryHostUrl}/{RepositoryOwner}/{RepositoryName}"); - context.Information($"Git remote name : {Remote}"); - context.Information($"Git reference : {Ref}"); - context.Information($"Branch : {Branch}"); - context.Information($"Solution : {SolutionPath.GetFilename()}"); - context.Information($"Version : {Version}"); - context.Information($"Public release : {(IsPublicRelease ? "yes" : "no")}"); - context.Information($"Prerelease : {(IsPrerelease ? "yes" : "no")}"); - context.Information($"Latest version : {LatestVersion?.ToString() ?? "(none)"}"); - context.Information($"Latest stable version : {LatestStableVersion?.ToString() ?? "(none)"}"); - } - - /* - * Summary : Gets a value indicating under which CI platform (if any) the script runs - */ - public CIPlatform CIPlatform { get; } - - /* - * Summary : Gets the repository host URL (e.g. "https://github.com" for a repository hosted on GitHub.) - */ - public string RepositoryHostUrl { get; } - - /* - * Summary : Gets the repository owner (e.g. "Tenacom" for repository Tenacom/SomeLibrary.) - */ - public string RepositoryOwner { get; } - - /* - * Summary : Gets the repository owner (e.g. "SomeLibrary" for repository Tenacom/SomeLibrary.) - */ - public string RepositoryName { get; } - - /* - * Summary : Gets the name of the Git remote that points to the main repository - * (usually "origin" in cloud builds, "upstream" when working locally on a fork.) - */ - public string Remote { get; } - - /* - * Summary : Gets Git's HEAD reference or SHA. - */ - public string Ref { get; private set; } - - /* - * Summary : Gets Git's HEAD branch name, or the empty string if not on a branch. - */ - public string Branch { get; } - - /* - * Summary : Gets the path of the directory where build artifacts are stored. - */ - public DirectoryPath ArtifactsPath { get; } - - /* - * Summary : Gets the path of the directory where test results and coverage reports are stored. - */ - public DirectoryPath TestResultsPath { get; } - - /* - * Summary : Gets the path of the solution file. - */ - public FilePath SolutionPath { get; } - - /* - * Summary : Gets the parsed solution. - */ - public SolutionParserResult Solution { get; } - - /* - * Summary : Gets the configuration to build. - */ - public string Configuration { get; } - - /* - * Summary : Gets the version to build, as a string computed by Nerdbank.GitVersioning. - */ - public string VersionStr { get; private set; } - - /* - * Summary : Gets the version to build, as a SemanticVersion object. - */ - public SemanticVersion Version { get; private set; } - - /* - * Summary : Gets the latest version published, if any, as a SemanticVersion object. - */ - public SemanticVersion? LatestVersion { get; private set; } - - /* - * Summary : Gets the latest stable version published, if any, as a SemanticVersion object. - */ - public SemanticVersion? LatestStableVersion { get; private set; } - - /* - * Summary : Gets a value that indicates whether a public release can be built. - * Value : True if Git's HEAD is on a public release branch, as indicated in version.json; - * otherwise, false. - */ - public bool IsPublicRelease { get; private set; } - - /* - * Summary : Gets a value that indicates whether the version to build is a prerelease. - */ - public bool IsPrerelease { get; private set; } - - /* - * Summary : Gets the MSBuild settings to use for DotNet aliases. - */ - public DotNetMSBuildSettings MSBuildSettings { get; } - - /* - * Summary : Update build configuration data, typically after a commit. - * Params : context - The Cake context. - */ - public void Update(ICakeContext context) - { - (VersionStr, Ref, IsPublicRelease, IsPrerelease) = context.GetVersionInformation(); - Version = SemanticVersion.Parse(VersionStr); - context.Information("Updated build configuration data:"); - context.Information($"Git reference : {Ref}"); - context.Information($"Version : {Version}"); - context.Information($"Public release : {(IsPublicRelease ? "yes" : "no")}"); - context.Information($"Prerelease : {(IsPrerelease ? "yes" : "no")}"); - } -} diff --git a/build/Changelog.cake b/build/Changelog.cake deleted file mode 100644 index 1c7a9f0..0000000 --- a/build/Changelog.cake +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Changelog management helpers -// --------------------------------------------------------------------------------------------- - -using System.Globalization; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; - -using SysFile = System.IO.File; - -sealed class Changelog -{ - public const string FileName = "CHANGELOG.md"; - - private readonly ICakeContext _context; - private readonly BuildData _data; - - /* - * Summary : Initializes a new instance of class Changelog. - * Params : context - The Cake context. - */ - public Changelog(ICakeContext context, BuildData data) - { - _context = context; - _data = data; - Path = new FilePath(FileName); - FullPath = Path.FullPath; - Exists = SysFile.Exists(FullPath); - } - - public FilePath Path { get; } - - public string FullPath { get; } - - public bool Exists { get; } - - /* - * Summary : Checks the changelog for contents in the "Unreleased changes" section. - * Params : (none) - * Returns : If there are any contents (excluding blank lines and sub-section headings) - * in the "Unreleased changes" section, true; otherwise, false. - */ - public bool HasUnreleasedChanges() - { - if (!Exists) - { - return false; - } - - using (var reader = new StreamReader(FullPath, Encoding.UTF8)) - { - var sectionHeadingRegex = new Regex(@"^ {0,3}##($|[^#])", RegexOptions.Compiled | RegexOptions.CultureInvariant); - var subSectionHeadingRegex = new Regex(@"^ {0,3}###($|[^#])", RegexOptions.Compiled | RegexOptions.CultureInvariant); - string? line; - do - { - line = reader.ReadLine(); - } while (line != null && !sectionHeadingRegex.IsMatch(line)); - - Ensure(_context, line != null, $"{FileName} contains no sections."); - for (; ;) - { - line = reader.ReadLine(); - if (line == null || sectionHeadingRegex.IsMatch(line)) - { - break; - } - - if (!string.IsNullOrWhiteSpace(line) && !subSectionHeadingRegex.IsMatch(line)) - { - return true; - } - } - } - - return false; - } - - /* - * Summary : Prepares the changelog for a release by moving the contents of the "Unreleased changes" section - * to a new section. - * Params : (none) - */ - public void PrepareForRelease() - { - _context.Information("Updating changelog..."); - var encoding = new UTF8Encoding(false, true); - var sb = new StringBuilder(); - using (var reader = new StreamReader(FullPath, encoding)) - using (var writer = new StringWriter(sb, CultureInfo.InvariantCulture)) - { - // Using a StringWriter instead of a StringBuilder allows for a custom line separator - // Under Windows, a StringBuilder would only use "\r\n" as a line separator, which would be wrong in this case - writer.NewLine = "\n"; - var sectionHeadingRegex = new Regex(@"^ {0,3}##($|[^#])", RegexOptions.Compiled | RegexOptions.CultureInvariant); - var subSectionHeadingRegex = new Regex(@"^ {0,3}###($|[^#])", RegexOptions.Compiled | RegexOptions.CultureInvariant); - var subSections = new List<(string Header, List Lines)>(); - subSections.Add(("", new List())); - var subSectionIndex = 0; - - const int ReadingFileHeader = 0; - const int ReadingUnreleasedChangesSection = 1; - const int ReadingRemainderOfFile = 2; - const int ReadingDone = 3; - var state = ReadingFileHeader; - while (state != ReadingDone) - { - var line = reader.ReadLine(); - switch (state) - { - case ReadingFileHeader: - Ensure(_context, line != null, $"{FileName} contains no sections."); - - // Copy everything up to an including the first section heading (which we assume is "Unreleased changes") - writer.WriteLine(line); - if (sectionHeadingRegex.IsMatch(line)) - { - state = ReadingUnreleasedChangesSection; - } - - break; - case ReadingUnreleasedChangesSection: - if (line == null) - { - // The changelog only contains the "Unreleased changes" section; - // this happens when no release has been published yet - WriteNewSections(true); - state = ReadingDone; - break; - } - - if (sectionHeadingRegex.IsMatch(line)) - { - // Reached header of next section - WriteNewSections(false); - writer.WriteLine(line); - state = ReadingRemainderOfFile; - break; - } - - if (subSectionHeadingRegex.IsMatch(line)) - { - subSections.Add((line, new List())); - ++subSectionIndex; - break; - } - - subSections[subSectionIndex].Lines.Add(line); - break; - case ReadingRemainderOfFile: - if (line == null) - { - state = ReadingDone; - break; - } - - writer.WriteLine(line); - break; - default: - Fail(_context, $"Internal error: reading state corrupted ({state})."); - throw null; - } - } - - void WriteNewSections(bool atEndOfFile) - { - // Create empty sub-sections in new "Unreleased changes" section - foreach (var subSection in subSections.Skip(1)) - { - writer.WriteLine(string.Empty); - writer.WriteLine(subSection.Header); - } - - // Write header of new release section - writer.WriteLine(string.Empty); - writer.WriteLine("## " + MakeSectionTitle()); - - var newSectionLines = CollectNewSectionLines(); - var newSectionCount = newSectionLines.Count; - if (atEndOfFile) - { - // If there is no other section after the new release, - // we don't want extra blank lines at EOF - while (newSectionCount > 0 && string.IsNullOrEmpty(newSectionLines[newSectionCount - 1])) - { - --newSectionCount; - } - } - - foreach (var newSectionLine in newSectionLines.Take(newSectionCount)) - { - writer.WriteLine(newSectionLine); - } - } - - List CollectNewSectionLines() - { - var result = new List(subSections[0].Lines); - - // Copy only sub-sections that have actual content - foreach (var subSection in subSections.Skip(1).Where(s => s.Lines.Any(l => !string.IsNullOrWhiteSpace(l)))) - { - result.Add(subSection.Header); - foreach (var contentLine in subSection.Lines) - { - result.Add(contentLine); - } - } - - return result; - } - } - - SysFile.WriteAllText(FullPath, sb.ToString(), encoding); - } - - /* - * Summary : Updates the heading of the first section of the changelog after the "Unreleased changes" section - * to reflect a change in the released version. - * Params : (none) - */ - public void UpdateNewSectionTitle() - { - _context.Information("Updating changelog's new release section title..."); - var encoding = new UTF8Encoding(false, true); - var sb = new StringBuilder(); - using (var reader = new StreamReader(FullPath, encoding)) - using (var writer = new StringWriter(sb, CultureInfo.InvariantCulture)) - { - // Using a StringWriter instead of a StringBuilder allows for a custom line separator - // Under Windows, a StringBuilder would only use "\r\n" as a line separator, which would be wrong in this case - writer.NewLine = "\n"; - var sectionHeadingRegex = new Regex(@"^ {0,3}##($|[^#])", RegexOptions.Compiled | RegexOptions.CultureInvariant); - - const int ReadingFileHeader = 0; - const int ReadingUnreleasedChangesSection = 1; - const int ReadingRemainderOfFile = 2; - const int ReadingDone = 3; - var state = ReadingFileHeader; - while (state != ReadingDone) - { - var line = reader.ReadLine(); - switch (state) - { - case ReadingFileHeader: - Ensure(_context, line != null, $"{FileName} contains no sections."); - writer.WriteLine(line); - if (sectionHeadingRegex.IsMatch(line)) - { - state = ReadingUnreleasedChangesSection; - } - - break; - case ReadingUnreleasedChangesSection: - Ensure(_context, line != null, $"{FileName} contains only one section."); - if (sectionHeadingRegex.IsMatch(line)) - { - // Replace header of second section - writer.WriteLine("## " + MakeSectionTitle()); - state = ReadingRemainderOfFile; - break; - } - - writer.WriteLine(line); - break; - case ReadingRemainderOfFile: - if (line == null) - { - state = ReadingDone; - break; - } - - writer.WriteLine(line); - break; - default: - Fail(_context, $"Internal error: reading state corrupted ({state})."); - throw null; - } - } - } - - SysFile.WriteAllText(FullPath, sb.ToString(), encoding); - } - - private string MakeSectionTitle() - { - return $"[{_data.VersionStr}](https://github.com/{_data.RepositoryOwner}/{_data.RepositoryName}/releases/tag/{_data.VersionStr}) ({DateTime.Now:yyyy-MM-dd})"; - } -} diff --git a/build/DocFx.cake b/build/DocFx.cake deleted file mode 100644 index 5c50c19..0000000 --- a/build/DocFx.cake +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#tool nuget:?package=docfx.console&version=2.59.4 - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// DocFx class -// --------------------------------------------------------------------------------------------- - -using System; -using System.Runtime.InteropServices; - -/* - * Summary : Implements DocFx operations - */ -sealed class DocFx -{ - private const string ToolExeName = "docfx.exe"; - - private readonly DirectoryPath _docsPath; - private FilePath? _docFxPath; - - /* - * Summary : Initializes a new instance of the DocFx class. - * Params : context - The Cake context. - * docsPath - The path to the folder where DocFX operates. - */ - public DocFx(ICakeContext context, BuildData buildData, DirectoryPath docsPath) - { - Context = context; - BuildData = buildData; - _docsPath = docsPath; - } - - private ICakeContext Context { get; } - - private BuildData BuildData { get; } - - /* - * Summary : Extracts language metadata according to docfx.json settings. - */ - public void Metadata() - { - var docFxJsonPath = _docsPath.CombineWithFilePath("docfx.json"); - var json = LoadJsonObject(docFxJsonPath); - if (!json.TryGetPropertyValue("metadata", out _)) - { - Context.Information("No metadata to generate."); - return; - } - - Context.Information("Running DocFx..."); - Run("metadata"); - } - - /* - * Summary : Generates documentation according to docfx.json settings. - */ - public void Build() - { - Context.Information("Running DocFx..."); - Run("build"); - } - - /* - * Summary : Hosts the built documentation web site. - */ - public void Serve() - { - if (BuildData.CIPlatform is not CIPlatform.None) - { - Context.Information("DocFX web server not suitable for CI builds, skipping."); - return; - } - - Context.Information("Starting DocFX web server..."); - var (_, process) = Start("serve _site"); - Console.WriteLine("Press any key to stop serving..."); - _ = WaitForKey(); - Context.Information("Stopping DocFX web server..."); - process.Kill(); - process.WaitForExit(); - } - - private static ConsoleKeyInfo WaitForKey() - { - while (Console.KeyAvailable) - { - _ = Console.ReadKey(true); - } - - return Console.ReadKey(true); - } - - private void Run(ProcessArgumentBuilder arguments) - { - var (commandName, process) = Start(arguments); - process.WaitForExit(); - var exitCode = process.GetExitCode(); - Context.Ensure(exitCode == 0, $"{commandName} exited with code {exitCode}."); - } - - private (string commandName, IProcess Process) Start(ProcessArgumentBuilder arguments) - { - _docFxPath ??= Context.Tools.Resolve(ToolExeName); - Context.Ensure(_docFxPath != null, $"Cannot find {ToolExeName}"); - FilePath command = _docFxPath; - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - command = "mono"; - arguments = arguments.PrependQuoted(_docFxPath.FullPath); - } - - var process = Context.StartAndReturnProcess(command, new ProcessSettings() - { - Arguments = arguments, - WorkingDirectory = _docsPath, - }); - - return (command.GetFilenameWithoutExtension().ToString(), process); - } -} diff --git a/build/THIRD-PARTY-NOTICES b/build/THIRD-PARTY-NOTICES deleted file mode 100644 index 4b11eb5..0000000 --- a/build/THIRD-PARTY-NOTICES +++ /dev/null @@ -1,34 +0,0 @@ -These scripts may use and/or incorporate third-party libraries or other resources -that may be distributed under licenses different than this project. - -In the event that we accidentally failed to list a required notice, please -bring it to our attention. Either post an issue, or email us: - - info@tenacom.it - -The attached notices are provided for information only. - -================================================================================================ -Humanizer - https://github.com/Humanizr/Humanizer ------------------------------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/build/dotnet.cake b/build/dotnet.cake deleted file mode 100644 index fc00b36..0000000 --- a/build/dotnet.cake +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// .NET SDK helpers -// --------------------------------------------------------------------------------------------- - -using System.IO; -using System.Linq; - -using SysDirectory = System.IO.Directory; -using SysPath = System.IO.Path; - -/* - * Summary : Restore all NuGet packages for the solution. - * Params : context - The Cake context. - * data - Build configuration data. - */ -static void RestoreSolution(this ICakeContext context, BuildData data) -{ - context.Information("Restoring NuGet packages for solution..."); - context.DotNetRestore(data.SolutionPath.FullPath, new() { - MSBuildSettings = data.MSBuildSettings, - DisableParallel = true, - Interactive = false, - }); -} - -/* - * Summary : Build all projects in the solution. - * Params : context - The Cake context. - * data - Build configuration data. - * restore - true to restore NuGet packages before building, false otherwise. - */ -static void BuildSolution(this ICakeContext context, BuildData data, bool restore) -{ - context.Information($"Building solution (restore = {restore})..."); - context.DotNetBuild(data.SolutionPath.FullPath, new() { - Configuration = data.Configuration, - MSBuildSettings = data.MSBuildSettings, - NoLogo = true, - NoRestore = !restore, - }); -} - -/* - * Summary : Run all unit tests for the solution. - * Params : context - The Cake context. - * data - Build configuration data. - * restore - true to restore NuGet packages before testing, false otherwise. - * build - true to build the solution before testing, false otherwise. - * collect - true to collect coverage data with Coverlet - * Remarks : If successful, this method will merge all coverage reports generated by VSTest - * into a single file suitable for upload to Codecov. - */ -static void TestSolution(this ICakeContext context, BuildData data, bool restore, bool build, bool collect) -{ - context.Information($"Running tests (restore = {restore}, build = {build}, collect = {collect})..."); - context.DotNetTest(data.SolutionPath.FullPath, new() { - Configuration = data.Configuration, - MSBuildSettings = data.MSBuildSettings, - NoBuild = !build, - NoLogo = true, - NoRestore = !restore, - ArgumentCustomization = args => collect - ? args.Append("--collect:\"XPlat Code Coverage\"") - : args, - }); - - // Merge coverage reports only if there are any - if (collect) - { - if (!context.FileSystem.Exist(data.TestResultsPath) || !context.GetSubDirectories(data.TestResultsPath).Any()) - { - context.Information("No coverage reports were generated."); - } - else - { - context.Information("Merging coverage reports..."); - const string CoverageDataFileName = "coverage.cobertura.xml"; - var coverageDataGlob = SysPath.Combine(data.TestResultsPath.FullPath, "*", CoverageDataFileName); - context.DotNetTool($"reportgenerator \"-reports:{coverageDataGlob}\" \"-targetDir:{data.TestResultsPath.FullPath}\" -reporttypes:Cobertura"); - } - } -} - -/* - * Summary : Run the Pack target on the solution. This usually produces NuGet packages, - * but Buildvana SDK may hijack the target to produce, for example, setup executables. - * Params : context - The Cake context. - * data - Build configuration data. - * restore - true to restore NuGet packages before packing, false otherwise. - * build - true to build the solution before packing, false otherwise. - */ -static void PackSolution(this ICakeContext context, BuildData data, bool restore, bool build) -{ - context.Information($"Packing solution (restore = {restore}, build = {build})..."); - context.DotNetPack(data.SolutionPath.FullPath, new() { - Configuration = data.Configuration, - MSBuildSettings = data.MSBuildSettings, - NoBuild = !build, - NoLogo = true, - NoRestore = !restore, - }); -} - -/* - * Summary : Asynchronously pushes all produced NuGet packages to the appropriate NuGet server. - * Params : context - The Cake context. - * data - Build configuration data. - * Remarks : - This method uses the following environment variables: - * * PRERELEASE_NUGET_SOURCE - NuGet source URL where to push prerelease packages - * * RELEASE_NUGET_SOURCE - NuGet source URL where to push non-prerelease packages - * * PRERELEASE_NUGET_KEY - API key for PRERELEASE_NUGET_SOURCE - * * RELEASE_NUGET_KEY - API key for RELEASE_NUGET_SOURCE - * - If there are no .nupkg files in the designated artifacts directory, this method does nothing. - */ -static async Task NuGetPushAllAsync(this ICakeContext context, BuildData data) -{ - const string nupkgMask = "*.nupkg"; - if (!SysDirectory.EnumerateFiles(data.ArtifactsPath.FullPath, nupkgMask).Any()) - { - context.Verbose("No .nupkg files to push."); - return; - } - - var isPrivate = await context.IsPrivateRepositoryAsync(data); - var nugetSource = context.GetOptionOrFail(isPrivate ? "privateNugetSource" : data.IsPrerelease ? "prereleaseNugetSource" : "releaseNugetSource"); - var nugetApiKey = context.GetOptionOrFail(isPrivate ? "privateNugetKey" : data.IsPrerelease ? "prereleaseNugetKey" : "releaseNugetKey"); - var nugetPushSettings = new DotNetNuGetPushSettings { - ForceEnglishOutput = true, - Source = nugetSource, - ApiKey = nugetApiKey, - SkipDuplicate = true, - }; - - var packages = SysPath.Combine(data.ArtifactsPath.FullPath, nupkgMask); - foreach (var path in context.GetFiles(packages)) - { - context.Information($"Pushing {path} to {nugetSource}..."); - context.DotNetNuGetPush(path, nugetPushSettings); - } -} diff --git a/build/environment.cake b/build/environment.cake deleted file mode 100644 index 9c52d8f..0000000 --- a/build/environment.cake +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Environment helpers -// --------------------------------------------------------------------------------------------- - -/* - * Summary : Gets a string from the environment, failing if the value is not found or is the empty string. - * Params : context - The Cake context. - * name - The name of the environment variable to read. - * fallbackName - The name of another environment variable to read if name is not found or its value is the empty string. - * Returns : The value of an environment variable. - */ -static string GetEnvironmentString(this ICakeContext context, string name, string fallbackName = "") -{ - var result = context.EnvironmentVariable(name, string.Empty); - if (!string.IsNullOrEmpty(result)) - { - return result; - } - - context.Ensure(!string.IsNullOrEmpty(fallbackName), $"Environment variable {name} is missing or has an empty value."); - result = context.EnvironmentVariable(fallbackName, string.Empty); - context.Ensure(!string.IsNullOrEmpty(result), 255, $"Both environment variables {name} and {fallbackName} are missing or have an empty value."); - return result; -} diff --git a/build/fail.cake b/build/fail.cake deleted file mode 100644 index 4fa821d..0000000 --- a/build/fail.cake +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Build failure helpers -// --------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; - -/* - * Summary : Fails the build with the specified message. - * This method does not return. - * Params : context - The Cake context. - * message - A message explaining the reason for failing the build. - */ -[DoesNotReturn] -static void Fail(this ICakeContext context, string message) -{ - context.Error(message); - throw new CakeException(message); -} - -/* - * Summary : Fails the build with the specified message. - * This method does not return. - * Type : T - The expected return type. - * Params : context - The Cake context. - * message - A message explaining the reason for failing the build. - * Returns : This method never returns. - */ -[DoesNotReturn] -static T Fail(this ICakeContext context, string message) -{ - context.Error(message); - throw new CakeException(message); -} - -/* - * Summary : Fails the build with the specified exit code and message. - * This method does not return. - * Params : context - The Cake context. - * exitCode - The Cake exit code. - * message - A message explaining the reason for failing the build. - */ -[DoesNotReturn] -static void Fail(this ICakeContext context, int exitCode, string message) -{ - context.Error(message); - throw new CakeException(exitCode, message); -} - -/* - * Summary : Fails the build with the specified exit code and message. - * This method does not return. - * Type : T - The expected return type. - * Params : context - The Cake context. - * exitCode - The Cake exit code. - * message - A message explaining the reason for failing the build. - * Returns : This method never returns. - */ -[DoesNotReturn] -static T Fail(this ICakeContext context, int exitCode, string message) -{ - context.Error(message); - throw new CakeException(exitCode, message); -} - -/* - * Summary : Fails the build with the specified message if a condition is not verified. - * Params : context - The Cake context. - * condition - The condition to verify. - * message - A message explaining the reason for failing the build. - */ -static void Ensure(this ICakeContext context, [DoesNotReturnIf(false)] bool condition, string message) -{ - if (!condition) - { - context.Error(message); - throw new CakeException(message); - } -} - -/* - * Summary : Fails the build with the specified message if a condition is not verified. - * Params : context - The Cake context. - * condition - The condition to verify. - * exitCode - The Cake exit code. - * message - A message explaining the reason for failing the build. - */ -static void Ensure(this ICakeContext context, [DoesNotReturnIf(false)] bool condition, int exitCode, string message) -{ - if (!condition) - { - context.Error(message); - throw new CakeException(exitCode, message); - } -} diff --git a/build/filesystem.cake b/build/filesystem.cake deleted file mode 100644 index 1a541f9..0000000 --- a/build/filesystem.cake +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// File system helpers -// --------------------------------------------------------------------------------------------- - -/* - * Summary : Delete a directory, including its contents, if it exists. - * Params : context - The Cake context. - * directory - The directory to delete. - */ -static void DeleteDirectoryIfExists(this ICakeContext context, DirectoryPath directory) -{ - if (!context.DirectoryExists(directory)) - { - context.Verbose($"Skipping non-existent directory: {directory}"); - return; - } - - context.Information($"Deleting directory: {directory}"); - context.DeleteDirectory(directory, new() { Force = false, Recursive = true }); -} diff --git a/build/git.cake b/build/git.cake deleted file mode 100644 index 2c84284..0000000 --- a/build/git.cake +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Git repository helpers -// --------------------------------------------------------------------------------------------- - -using System; -using System.Linq; - -/* - * Summary : Gets the name of the current Git branch. - * Params : context - The Cake context. - * Returns : If HEAD is on a branch, the name of the branch; otherwise, the empty string. - */ -static string GetCurrentGitBranch(this ICakeContext context) => context.Exec("git", "branch --show-current").FirstOrDefault(string.Empty); - -/* - * Summary : Attempts to get information about the remote repository. - * Params : context - The Cake context. - * Returns : Remote - The Git remote name. - * HostUrl - The base URL of the Git repository host. - * Owner - The repository owner. - * Name - The repository name. - * Remarks : - If the githubRepository argument is given, or the GITHUB_REPOSITORY environment variable is set - * (as it happens in GitHub Actions,) Owner and Name are taken from there, while Remote is set - * to the first Git remote found whose fetch URL matches them. - * - If GITHUB_REPOSITORY is not available, Git remote fetch URLs are parsed for Owner and Name; - * remotes "upstream" and "origin" are tested, in that order, in case "origin" is a fork. - */ -static bool TryGetRepositoryInfo(this ICakeContext context, out (string Remote, string HostUrl, string Owner, string Name) result) -{ - return TryGetRepositoryInfoFromGitHubActions(out result) - || TryGetRepositoryInfoFromGitRemote("upstream", out result) - || TryGetRepositoryInfoFromGitRemote("origin", out result); - - bool TryGetRepositoryInfoFromGitHubActions(out (string Remote, string HostUrl, string Owner, string Name) result) - { - var repository = context.GetOption("githubRepository", string.Empty); - if (string.IsNullOrEmpty(repository)) - { - result = default; - return false; - } - - var hostUrl = context.GetOptionOrFail("githubServerUrl"); - var segments = repository.Split('/'); - foreach (var remote in context.Exec("git", "remote")) - { - if (TryGetRepositoryInfoFromGitRemote(remote, out result) - && string.Equals(result.HostUrl, hostUrl, StringComparison.Ordinal) - && string.Equals(result.Owner, segments[0], StringComparison.Ordinal) - && string.Equals(result.Name, segments[1], StringComparison.Ordinal)) - { - return true; - } - } - - result = default; - return false; - } - - bool TryGetRepositoryInfoFromGitRemote(string remote, out (string Remote, string HostUrl, string Owner, string Name) result) - { - if (context.Exec("git", "remote get-url " + remote, out var output) != 0) - { - result = default; - return false; - } - - var url = output.FirstOrDefault(); - if (string.IsNullOrEmpty(url)) - { - result = default; - return false; - } - - Uri uri; - try - { - uri = new Uri(url); - } - catch (UriFormatException) - { - result = default; - return false; - } - - var path = uri.AbsolutePath; - path = path.EndsWith(".git", StringComparison.Ordinal) - ? path.Substring(1, path.Length - 5) - : path.Substring(1); - - var segments = path.Split('/'); - if (segments.Length != 2) - { - result = default; - return false; - } - - result = (remote, $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? null : ":" + uri.Port.ToString())}", segments[0], segments[1]); - return true; - } -} - -/* - * Summary : Tells whether a tag exists in the local Git repository. - * Params : context - The Cake context. - * tag - The tag to check for. - * Returns : True if the tag exists; false otherwise. - */ -static bool GitTagExists(this ICakeContext context, string tag) => context.Exec("git", "tag").Any(s => string.Equals(tag, s, StringComparison.Ordinal)); - -/* - * Summary : Gets the latest version and the latest stable version in commit history. - * Params : context - The Cake context. - * Returns : A tuple of the latest version and the latest stable version; - * Remarks : - If no version tag is found in commit history, this method returns a tuple of two nulls. - * - If no stable version tag is found in commit history, this method returns a tuple of the latest version and null. - */ -static (SemanticVersion? Latest, SemanticVersion? LatestStable) GitGetLatestVersions(this ICakeContext context) -{ - context.Verbose("Looking for latest stable version tag in Git commit history..."); - var output = context.Exec("git", "log --pretty=format:%D"); - var versions = output.Where(static x => !string.IsNullOrEmpty(x)) - .SelectMany(static x => x.Split(", ")) - .Where(static x => x.StartsWith("tag: ")) - .Select(static x => x.Substring(5)) - .Select(static x => { - _ = SemanticVersion.TryParse(x, out var version); - return version; - }) - .WhereNotNull(); - - SemanticVersion? latest = null; - SemanticVersion? latestStable = null; - foreach (var version in versions) - { - if (latest == null) - { - latest = version; - } - - if (!version.IsPrerelease) - { - latestStable = version; - break; - } - } - - return (latest, latestStable); -} - -/* - * Summary : Sets Git user name and email. - * Params : context - The Cake context. - * name - The name of the user. - * email - The email address of the user. - */ -static void GitSetUserIdentity(this ICakeContext context, string name, string email) -{ - context.Information($"Setting Git user name to '{name}'..."); - _ = context.Exec( - "git", - new ProcessArgumentBuilder() - .Append("config") - .Append("user.name") - .AppendQuoted(name)); - - context.Information($"Setting Git user email to '{email}'..."); - _ = context.Exec( - "git", - new ProcessArgumentBuilder() - .Append("config") - .Append("user.email") - .AppendQuoted(email)); -} diff --git a/build/github.cake b/build/github.cake deleted file mode 100644 index e5b63cb..0000000 --- a/build/github.cake +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#addin nuget:?package=Cake.Http&version=4.0.0 -#addin nuget:?package=Octokit&version=9.1.2 - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// GitHub API helpers -// --------------------------------------------------------------------------------------------- - -using System.Threading.Tasks; -using Octokit; - -using SysFile = System.IO.File; -using SysPath = System.IO.Path; - -/* - * Summary : Asynchronously gets the visibility of the current repository. - * Params : context - The Cake context. - * data - Build configuration data. - * Returns : A Task that represents the ongoing operation. - */ -static async Task IsPrivateRepositoryAsync(this ICakeContext context, BuildData data) -{ - context.Information("Fetching repository information..."); - var client = context.CreateGitHubClient(); - var repository = await client.Repository.Get(data.RepositoryOwner, data.RepositoryName).ConfigureAwait(false); - return repository.Private; -} - -/* - * Summary : Asynchronously creates a new draft release on the GitHub repository. - * Params : context - The Cake context. - * data - Build configuration data. - * Returns : A Task, representing the ongoing operation, whose value will be an object representing the newly created release. - */ -static async Task CreateDraftReleaseAsync(this ICakeContext context, BuildData data) -{ - var tag = data.VersionStr; - var client = context.CreateGitHubClient(); - context.Information($"Creating a provisional draft release..."); - var newRelease = new NewRelease(tag) - { - Name = $"{tag} [provisional]", - TargetCommitish = data.Branch, - Prerelease = data.IsPrerelease, - Draft = true, - }; - - return await client.Repository.Release.Create(data.RepositoryOwner, data.RepositoryName, newRelease).ConfigureAwait(false); -} - -/* - * Summary : Asynchronously uploads a release asset. - * Params : context - The Cake context. - * data - Build configuration data. - * release - An object representing the release. - * path - The full path of the asset file. - * mimeType - The MIME type of the asset. - * description - A short textual description of the asset. - * Returns : A Task that represents the ongoing operation. - */ -static async Task UploadReleaseAssetAsync(this ICakeContext context, BuildData data, Release release, string path, string mimeType, string description) -{ - var tag = data.VersionStr; - var client = context.CreateGitHubClient(); - context.Verbose($"Uploading asset {path}..."); - ReleaseAsset asset; - await using (var assetContents = SysFile.OpenRead(path)) - { - var upload = new ReleaseAssetUpload() - { - FileName = SysPath.GetFileName(path), - ContentType = mimeType, - RawData = assetContents, - }; - - asset = await client.Repository.Release.UploadAsset(release, upload).ConfigureAwait(false); - } - - if (!string.IsNullOrEmpty(description)) - { - context.Verbose("Updating asset label..."); - var update = asset.ToUpdate(); - update.Label = description; - _ = await client.Repository.Release.EditAsset(data.RepositoryOwner, data.RepositoryName, asset.Id, update).ConfigureAwait(false); - } - else - { - context.Verbose("Skipping label update: asset has no description."); - } - -} - -/* - * Summary : Asynchronously publishes a draft release on the GitHub repository. - * Params : context - The Cake context. - * data - Build configuration data. - * release - An object representing the release. - * Returns : A Task that represents the ongoing operation. - */ -static async Task PublishReleaseAsync(this ICakeContext context, BuildData data, Release release) -{ - var tag = data.VersionStr; - var client = context.CreateGitHubClient(); - context.Information($"Generating release notes for {tag}..."); - var releaseNotesRequest = new GenerateReleaseNotesRequest(tag) - { - TargetCommitish = data.Branch, - }; - - var generateNotesResponse = await client.Repository.Release.GenerateReleaseNotes(data.RepositoryOwner, data.RepositoryName, releaseNotesRequest).ConfigureAwait(false); - var body = $"We also have a [human-curated changelog]({data.RepositoryHostUrl}/{data.RepositoryOwner}/{data.RepositoryName}/blob/main/CHANGELOG.md).\n\n---\n\n" - + generateNotesResponse.Body; - - context.Information($"Publishing the previously created release as {tag}..."); - var update = release.ToUpdate(); - update.TagName = tag; - update.Name = tag; - update.Body = body; - update.Prerelease = data.IsPrerelease; - update.Draft = false; - - _ = await client.Repository.Release.Edit(data.RepositoryOwner, data.RepositoryName, release.Id, update).ConfigureAwait(false); -} - -/* - * Summary : Asynchronously deletes a release and, optionally, the corresponding tag on the GitHub repository. - * Params : context - The Cake context. - * data - Build configuration data. - * release - An object representing the release. - * tagName - The tag name, or null to not delete a tag. - * Returns : A Task that represents the ongoing operation. - */ -static async Task DeleteReleaseAsync(this ICakeContext context, BuildData data, Release release, string? tagName) -{ - context.Information("Deleting the previously created release..."); - var client = context.CreateGitHubClient(); - await client.Repository.Release.Delete(data.RepositoryOwner, data.RepositoryName, release.Id).ConfigureAwait(false); - if (tagName != null) - { - var reference = "refs/tags/" + tagName; - context.Information($"Looking for reference '{reference}' in GitHub repository..."); - try - { - _ = await client.Git.Reference.Get(data.RepositoryOwner, data.RepositoryName, reference).ConfigureAwait(false); - } - catch (NotFoundException) - { - context.Information($"Reference '{reference}' not found in GitHub repository."); - return; - } - - context.Information($"Deleting reference '{reference}' in GitHub repository..."); - await client.Git.Reference.Delete(data.RepositoryOwner, data.RepositoryName, reference).ConfigureAwait(false); - } -} - -/* - * Summary : Asynchronously creates a workflow dispatch event on the GitHub repository. - * Params : context - The Cake context. - * data - Build configuration data. - * filename - The name of the workflow file to run, including extension. - * ref - The branch or tag on which to dispatch the workflow run. - * inputs - An optional anonymous object containing the inputs for the workflow. - * Returns : A Task that represents the ongoing operation. - */ -static async Task DispatchWorkflow(this ICakeContext context, BuildData data, string filename, string @ref, object? inputs = null) -{ - context.Information($"Dispatching workflow '{filename}' on '{@ref}'..."); - object requestBody = inputs == null - ? new { @ref = @ref } - : new { @ref = @ref, inputs = inputs }; - - var httpSettings = new HttpSettings() - .SetAccept("application/vnd.github.v3") - .AppendHeader("Authorization", "Token " + context.GetOptionOrFail("githubToken")) - .AppendHeader("User-Agent", "Buildvana (Win32NT 10.0.19044; amd64; en-US)") - .SetJsonRequestBody(requestBody) - .EnsureSuccessStatusCode(true); - - _ = await context.HttpPostAsync($"https://api.github.com/repos/{data.RepositoryOwner}/{data.RepositoryName}/actions/workflows/{filename}/dispatches", httpSettings); -} - -/* - * Summary : Sets a GitHub Actions step output. - * Params : context - The Cake context. - * name - The output name. - * value - The output value. - */ -static void SetActionsStepOutput(this ICakeContext context, string name, string value) -{ - var outputFile = context.EnvironmentVariable("GITHUB_OUTPUT"); - context.Ensure(!string.IsNullOrEmpty(outputFile), "Cannot set Actions step output: GITHUB_OUTPUT not set."); - SysFile.AppendAllLines(outputFile, new[] { $"{name}={value}" }, Encoding.UTF8); -} - -static GitHubClient CreateGitHubClient(this ICakeContext context) -{ - var client = new GitHubClient(new ProductHeaderValue("Buildvana")); - client.Credentials = new Credentials(context.GetOptionOrFail("githubToken")); - return client; -} diff --git a/build/json.cake b/build/json.cake deleted file mode 100644 index 37e7879..0000000 --- a/build/json.cake +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// JSON helpers -// --------------------------------------------------------------------------------------------- - -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Nodes; - -using SysFile = System.IO.File; - -/* - * Summary : Parses a JSON object from a string. Fails the build if not successful. - * Params : context - The Cake context. - * str - The string to parse. - * description - A description of the string for exception messages. - * Returns : The parsed object. - */ -static JsonObject ParseJsonObject(this ICakeContext context, string str, string description = "The provided string") -{ - JsonNode? node; - try - { - node = JsonNode.Parse( - str, - new JsonNodeOptions { PropertyNameCaseInsensitive = false }, - new JsonDocumentOptions - { - AllowTrailingCommas = true, - CommentHandling = JsonCommentHandling.Skip, - }); - } - catch (JsonException) - { - context.Fail($"{description} is not valid JSON."); - throw null; - } - - return node switch { - null => context.Fail($"{description} was parsed as JSON null."), - JsonObject obj => obj, - object other => context.Fail($"{description} was parsed as a {other.GetType().Name}, not a {nameof(JsonObject)}."), - }; -} - -/* - * Summary : Loads a JSON object from a file. Fails the build if not successful. - * Params : context - The Cake context. - * path - The path of the file to parse. - * Returns : The parsed object. - */ -static JsonObject LoadJsonObject(this ICakeContext context, FilePath path) -{ - var fullPath = path.FullPath; - JsonNode? node; - try - { - using var stream = SysFile.OpenRead(fullPath); - node = JsonNode.Parse( - stream, - new JsonNodeOptions { PropertyNameCaseInsensitive = false }, - new JsonDocumentOptions - { - AllowTrailingCommas = true, - CommentHandling = JsonCommentHandling.Skip, - }); - } - catch (IOException e) - { - context.Fail($"Could not read from {fullPath}: {e.Message}"); - throw null; - } - catch (JsonException) - { - context.Fail($"{fullPath} does not contain valid JSON."); - throw null; - } - - return node switch { - null => context.Fail($"{fullPath} was parsed as JSON null."), - JsonObject obj => obj, - object other => context.Fail($"{fullPath} was parsed as a {other.GetType().Name}, not a {nameof(JsonObject)}."), - }; -} - -/* - * Summary : Saves a JSON object to a file. Fails the build if not successful. - * Params : context - The Cake context. - * path - The path of the file to parse. - * Returns : The parsed object. - */ -static void SaveJson(this ICakeContext context, JsonNode json, FilePath path) -{ - var fullPath = path.FullPath; - try - { - using var stream = SysFile.OpenWrite(fullPath); - var writerOptions = new JsonWriterOptions - { - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - Indented = true, - }; - - using var writer = new Utf8JsonWriter(stream, writerOptions); - json.WriteTo(writer); - stream.SetLength(stream.Position); - } - catch (IOException e) - { - context.Fail($"Could not write to {fullPath}: {e.Message}"); - throw null; - } -} - -/* - * Summary : Gets the value of a property from a JSON object. Fails the build if not successful. - * Types : T - The desired type of the property value. - * Params : context - The Cake context. - * json - The JSON object. - * propertyName - The name of the property to get. - * description - A description of the object for exception messages. - * Returns : The value of the specified property. - */ -static T GetJsonPropertyValue(this ICakeContext context, JsonObject json, string propertyName, string objectDescription = "JSON object") -{ - context.Ensure(json.TryGetPropertyValue(propertyName, out var property), $"Json property {propertyName} not found in {objectDescription}."); - switch (property) - { - case null: - return context.Fail($"Json property {propertyName} in {objectDescription} is null."); - case JsonValue value: - context.Ensure(value.TryGetValue(out var result), $"Json property {propertyName} in {objectDescription} cannot be converted to a {typeof(T).Name}."); - return result ?? context.Fail($"Json property {propertyName} in {objectDescription} has a null value."); - default: - return context.Fail($"Json property {propertyName} in {objectDescription} is a {property.GetType().Name}, not a {nameof(JsonValue)}."); - } -} diff --git a/build/nbgv.cake b/build/nbgv.cake deleted file mode 100644 index 62314b8..0000000 --- a/build/nbgv.cake +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Nerdbank.GitVersioning helpers -// --------------------------------------------------------------------------------------------- - -using System.Text; -using System.Text.Json.Nodes; - -/* - * Summary : Gets version information using the NBGV tool. - * Params : context - The Cake context. - * Returns : VersionStr - The project version. - * Ref - The Git ref from which we are building. - * IsPublicRelease - True if a public release can be built, false otherwise. - * IsPrerelease - True if the project version is tagged as prerelease, false otherwise. - */ -static (string VersionStr, string Ref, bool IsPublicRelease, bool IsPrerelease) GetVersionInformation(this ICakeContext context) -{ - var nbgvOutput = new StringBuilder(); - context.DotNetTool( - "nbgv get-version --format json", - new DotNetToolSettings { - SetupProcessSettings = s => s - .SetRedirectStandardOutput(true) - .SetRedirectedStandardOutputHandler(x => { - nbgvOutput.AppendLine(x); - return x; - }), - }); - - var json = context.ParseJsonObject(nbgvOutput.ToString(), "The output of nbgv"); - return ( - VersionStr: context.GetJsonPropertyValue(json, "SemVer2", "the output of nbgv"), - Ref: context.GetJsonPropertyValue(json, "BuildingRef", "the output of nbgv"), - IsPublicRelease: context.GetJsonPropertyValue(json, "PublicRelease", "the output of nbgv"), - IsPrerelease: !string.IsNullOrEmpty(context.GetJsonPropertyValue(json, "PrereleaseVersion", "the output of nbgv"))); -} diff --git a/build/options.cake b/build/options.cake deleted file mode 100644 index 27d06f5..0000000 --- a/build/options.cake +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Option helpers -// --------------------------------------------------------------------------------------------- - -using System.ComponentModel; -using System.Linq; - -/* - * Summary : Tells whether the specified option is present, either as an argument - * or as an environment variable. - * Params : context - The Cake context. - * name - The option name. - * environmentPrefix - An optional prefix for the environment variable name; - * for example, camelCasedOption (prefix = "MYAPP_") -> MYAPP_CAMEL_CASED_OPTION - * Returns : If an argument with the specified name is present, true; - * if an environment variable with the specified name (converted according to environmentPrefix) - * is present, true; otherwise, false. - */ -static bool HasOption(this ICakeContext context, string name, string? environmentPrefix = null) - => context.HasArgument(name) || context.HasEnvironmentVariable(OptionNameToEnvironmentVariableName(name, environmentPrefix)); - -/* - * Summary : Gets an option from, in this order: - * * a command line argument with the specified name; - * * an environment variable with the specified name converted to UNDERSCORE_UPPER_CASE; - * * the provided default value. - * Params : context - The Cake context. - * name - The option name. - * defaultValue - The value returned if neither a corresponding argument - * nor environment variable was found. - */ -static T GetOption(this ICakeContext context, string name, T defaultValue) - where T : notnull - => context.GetOption(name, null, defaultValue); - -/* - * Summary : Gets an option from, in this order: - * * a command line argument with the specified name; - * * an environment variable with the specified name converted to UNDERSCORE_UPPER_CASE - * and optionally prefixed with the specified environmentPrefix; - * * the provided default value. - * Params : context - The Cake context. - * name - The option name. - * environmentPrefix - An optional prefix for the environment variable name; - * for example, "camelCasedOption" with an environmentPrefix - * of "MYAPP_" becomes "MYAPP_CAMEL_CASED_OPTION". - * defaultValue - The value returned if neither a corresponding argument - * nor environment variable was found. - */ -static T GetOption(this ICakeContext context, string name, string? environmentPrefix, T defaultValue) - where T : notnull -{ - var value = context.Arguments.GetArguments(name)?.FirstOrDefault(); - if (value != null) - { - return ConvertOption(value); - } - - value = context.Environment.GetEnvironmentVariable(OptionNameToEnvironmentVariableName(name, environmentPrefix)); - return value == null ? defaultValue : ConvertOption(value); -} - -/* - * Summary : Gets an option from, in this order: - * * a command line argument with the specified name; - * * an environment variable with the specified name converted to UNDERSCORE_UPPER_CASE; - * * the provided default value. - * Throw an exception if the option is not found or has an empty value. - * Params : context - The Cake context. - * name - The option name. - */ -static T GetOptionOrFail(this ICakeContext context, string name) - where T : notnull - => context.GetOptionOrFail(name, null); - -/* - * Summary : Gets an option from, in this order: - * * a command line argument with the specified name; - * * an environment variable with the specified name converted to UNDERSCORE_UPPER_CASE - * and optionally prefixed with the specified environmentPrefix; - * * the provided default value. - * Throw an exception if the option is not found or has an empty value. - * Params : context - The Cake context. - * name - The option name. - * environmentPrefix - An optional prefix for the environment variable name; - * for example, "camelCasedOption" with an environmentPrefix - * of "MYAPP_" becomes "MYAPP_CAMEL_CASED_OPTION". - */ -static T GetOptionOrFail(this ICakeContext context, string name, string? environmentPrefix) - where T : notnull -{ - var value = context.Arguments.GetArguments(name)?.FirstOrDefault(); - if (value != null) - { - return ConvertOption(value); - } - - var envName = OptionNameToEnvironmentVariableName(name, environmentPrefix); - value = context.Environment.GetEnvironmentVariable(envName); - if (value != null) - { - return ConvertOption(value); - } - - throw new CakeException($"Option {name} / environment variable {envName} not found or empty."); -} - -/* - * Summary : Converts an option name (which is supposed to be in camelCase) - * to an environment variable name (UNDERSCORE_UPPER_CASE). - * Params : prefix - An optional prefix for the environment variable name; - * for example, camelCasedOption (prefix = "MYAPP_") -> MYAPP_CAMEL_CASED_OPTION - */ -// Copyright (c) .NET Foundation and Contributors - MIT License - https://github.com/Humanizr/Humanizer -static string OptionNameToEnvironmentVariableName(string name, string? prefix = null) - => (prefix ?? string.Empty) + Regex.Replace( - Regex.Replace( - Regex.Replace( - name, - @"([\p{Lu}]+)([\p{Lu}][\p{Ll}])", - "$1_$2"), - @"([\p{Ll}\d])([\p{Lu}])", - "$1_$2"), - @"[-\s]", - "_") - .ToUpperInvariant(); - -/* - * Summary : Convert an option to the desired type. - * Types : T - The type to convert the option to. - * Params : value - The value of the option. - * Returns : The converted value. - */ -static T ConvertOption(string value) - where T : notnull -{ - var converter = TypeDescriptor.GetConverter(typeof(T)); - return (T)converter.ConvertFromInvariantString(value)!; -} diff --git a/build/process.cake b/build/process.cake deleted file mode 100644 index 7631773..0000000 --- a/build/process.cake +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Process helpers -// --------------------------------------------------------------------------------------------- - -using System.Collections.Generic; - -/* - * Summary : Executes an external command, capturing standard output and failing if the exit code is not zero. - * Params : context - The Cake context. - * command - The name of the command to execute. - * arguments - The arguments to pass to the command. - * Returns : The captured output of the command. - */ -static IEnumerable Exec(this ICakeContext context, string command, ProcessArgumentBuilder arguments) -{ - var exitCode = context.Exec(command, arguments, out var output); - context.Ensure(exitCode == 0, $"'{command} {arguments.RenderSafe()}' exited with code {exitCode}."); - return output; -} - -/* - * Summary : Executes an external command, capturing standard output and failing if the exit code is not zero. - * Params : context - The Cake context. - * command - The name of the command to execute. - * arguments - The arguments to pass to the command. - * out output - The captured output of the command. - * Returns : The exit code of the command. - */ -static int Exec(this ICakeContext context, string command, ProcessArgumentBuilder arguments, out IEnumerable output) - => context.StartProcess( - command, - new ProcessSettings { Arguments = arguments, RedirectStandardOutput = true }, - out output); diff --git a/build/public-api.cake b/build/public-api.cake deleted file mode 100644 index fd1dfbe..0000000 --- a/build/public-api.cake +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Public API helpers -// --------------------------------------------------------------------------------------------- - -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using SysFile = System.IO.File; - -/* - * Summary : Specifies the kind of changes public APIs have undergone between an older and a newer version. - * Remarks : The values of this enum are sorted in ascending order of importance, - * so that they may be compared. - */ -enum ApiChangeKind -{ - /* - * Summary : Public APIs have not changed between two versions. - */ - None, - - /* - * Summary : A newer version has only added public APIs with respect to an older version. - */ - Additive, - - /* - * Summary : A newer version's public APIs have undergone breaking changes since an older version was published. - */ - Breaking, -} - -/* - * Summary : Gets the kind of change public APIs underwent, according to the presence of new public APIs - * and/or the removal of existing public APIs in all PublicAPI.Unshipped.txt files - * of the repository. - * Params : context - The Cake context. - * Returns : If at least one public API was removed, ApiChangeKind.Breaking; - * if no public API was removed, but at least one was added, ApiChangeKind.Additive; - * if no public API was removed nor added, ApiChangeKind.None. - */ -static ApiChangeKind GetPublicApiChangeKind(this ICakeContext context) -{ - context.Information("Computing API change kind according to unshipped public API files..."); - var result = ApiChangeKind.None; - foreach (var unshippedPath in context.GetAllPublicApiFilePairs().Select(pair => pair.UnshippedPath)) - { - var fileResult = context.GetPublicApiChangeKind(unshippedPath); - context.Verbose($"{unshippedPath} -> {fileResult}"); - if (fileResult == ApiChangeKind.Breaking) - { - return ApiChangeKind.Breaking; - } - else if (fileResult > result) - { - result = fileResult; - } - } - - return result; -} - -/* - * Summary : Transfers unshipped public API definitions to PublicAPI.Shipped.txt - * in all directories of the repository. - * Params : context - The Cake context. - * Returns : An enumeration of the modified files. - */ -static IEnumerable TransferAllPublicApiToShipped(this ICakeContext context) -{ - context.Information("Updating public API files..."); - foreach (var pair in context.GetAllPublicApiFilePairs()) - { - context.Verbose($"Updating {pair.ShippedPath}..."); - if (context.TransferPublicApiToShipped(pair.UnshippedPath, pair.ShippedPath)) - { - yield return pair.ShippedPath; - yield return pair.UnshippedPath; - } - } -} - -/* - * Summary : Gets all public API definition file pairs in the repository. - * Params : context - The Cake context. - * Returns : An enumeration of (UnshippedPath, ShippedPath) tuples. - */ -static IEnumerable<(FilePath UnshippedPath, FilePath ShippedPath)> GetAllPublicApiFilePairs(this ICakeContext context) -{ - (FilePath UnshippedPath, FilePath ShippedPath)? GetPair(FilePath shippedPath) - { - var unshippedPath = shippedPath.GetDirectory().CombineWithFilePath("PublicAPI.Unshipped.txt"); - return context.FileSystem.Exist(unshippedPath) ? (unshippedPath, shippedPath) : null; - } - - return context - .GetFiles("**/PublicAPI.Shipped.txt", new() { IsCaseSensitive = true }) - .Select(GetPair) - .Where(maybePair => maybePair.HasValue) - .Select(maybePair => maybePair!.Value); -} - -/* - * Summary : Gets the kind of change public APIs underwent, according to the presence of new public APIs - * and/or the removal of existing public APIs. - * Params : context - The Cake context. - * unshippedPath - The FilePath of PublicAPI.Unshipped.txt - * Returns : If at least one public API was removed, ApiChangeKind.Breaking; - * if no public API was removed, but at least one was added, ApiChangeKind.Additive; - * if no public API was removed nor added, ApiChangeKind.None. - */ -static ApiChangeKind GetPublicApiChangeKind(this ICakeContext context, FilePath unshippedPath) -{ - var unshippedLines = SysFile.ReadAllLines(unshippedPath.FullPath, Encoding.UTF8); - static bool IsEmptyOrStartsWithHash(string s) => s.Length == 0 || s[0] == '#'; - var unshippedPublicApiLines = unshippedLines.SkipWhile(IsEmptyOrStartsWithHash); - const string RemovedPrefix = "*REMOVED*"; - var newApiPresent = false; - foreach (var line in unshippedPublicApiLines) - { - if (line.StartsWith(RemovedPrefix, StringComparison.Ordinal)) - { - return ApiChangeKind.Breaking; - } - - newApiPresent = true; - } - - return newApiPresent ? ApiChangeKind.Additive : ApiChangeKind.None; -} - -/* - * Summary : Transfers unshipped public API definitions to PublicAPI.Shipped.txt - * Params : context - The Cake context. - * unshippedPath - The FilePath of PublicAPI.Unshipped.txt - * shippedPath - The FilePath of PublicAPI.Shipped.txt - * Returns : true if files were modified; false otherwise. - */ -static bool TransferPublicApiToShipped(this ICakeContext context, FilePath unshippedPath, FilePath shippedPath) -{ - var utf8 = new UTF8Encoding(false); - var unshippedLines = SysFile.ReadAllLines(unshippedPath.FullPath, utf8); - var unshippedHeaderLines = unshippedLines.TakeWhile(IsEmptyOrStartsWithHash).ToArray(); - if (unshippedHeaderLines.Length == unshippedLines.Length) - { - return false; - } - - static bool IsEmptyOrStartsWithHash(string s) => s.Length == 0 || s[0] == '#'; - var shippedLines = SysFile.ReadAllLines(shippedPath.FullPath, utf8); - var shippedHeaderLines = shippedLines.TakeWhile(IsEmptyOrStartsWithHash).ToArray(); - - const string RemovedPrefix = "*REMOVED*"; - static bool StartsWithRemovedPrefix(string s) => s.StartsWith(RemovedPrefix, StringComparison.Ordinal); - static bool DoesNotStartWithRemovedPrefix(string s) => !StartsWithRemovedPrefix(s); - var removedLines = unshippedLines - .Skip(unshippedHeaderLines.Length) - .Where(StartsWithRemovedPrefix) - .Select(l => l[(RemovedPrefix.Length)..]) - .OrderBy(l => l, StringComparer.Ordinal) // For BinarySearch - .ToArray(); - - bool IsNotRemoved(string s) => Array.BinarySearch(removedLines, s, StringComparer.Ordinal) < 0; - var newShippedLines = shippedLines - .Skip(shippedHeaderLines.Length) - .Where(IsNotRemoved) - .Concat(unshippedLines - .Skip(unshippedHeaderLines.Length) - .Where(DoesNotStartWithRemovedPrefix)) - .OrderBy(l => l, StringComparer.Ordinal); - - SysFile.WriteAllLines(shippedPath.FullPath, shippedHeaderLines.Concat(newShippedLines), utf8); - SysFile.WriteAllLines(unshippedPath.FullPath, unshippedHeaderLines, utf8); - return true; -} diff --git a/build/setup-teardown.cake b/build/setup-teardown.cake deleted file mode 100644 index 5a43231..0000000 --- a/build/setup-teardown.cake +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// ============================================================================================= -// Setup and Teardown, common to all scripts -// ============================================================================================= - -Setup(context => new BuildData(context)); - -Teardown((context, data) => -{ - // For some reason, DotNetBuildServerShutdown hangs in a GitHub Actions runner; - // it is still useful on a local machine though. - // TODO: Test whether it can be enabled in e.g. GitLab CI. - if (data.CIPlatform is CIPlatform.None) - { - context.DotNetBuildServerShutdown(new DotNetBuildServerShutdownSettings - { - Razor = true, - VBCSCompiler = true, - }); - } -}); diff --git a/build/utilities.cake b/build/utilities.cake deleted file mode 100644 index e4642fa..0000000 --- a/build/utilities.cake +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Miscellaneous utilities -// --------------------------------------------------------------------------------------------- - -using System; -using System.Linq; - -/* - * Summary : Filters a sequence of nullable values, taking only those that are not null. - * Type params : T - The type of the elements of this. - * Params : this - An IEnumerable to filter. - * Returns : An IEnumerable that contains elements from the input sequence that are not null. - */ -static IEnumerable WhereNotNull(this IEnumerable @this) - where T : class -{ - return @this.Where(IsNotNull) as IEnumerable; - - static bool IsNotNull(T? x) => x is not null; -} - -/* - * Summary : Filters a sequence of nullable values, taking only those that are not null. - * Type params : T - The type of the elements of this. - * Params : this - An IEnumerable to filter. - * Returns : An IEnumerable that contains elements from the input sequence that are not null. - */ -public static IEnumerable WhereNotNull(this IEnumerable @this) - where T : struct -{ - return @this.Where(IsNotNull).Select(GetValue); - - static bool IsNotNull(T? x) => x.HasValue; - - static T GetValue(T? x) => x!.Value; -} diff --git a/build/versioning.cake b/build/versioning.cake deleted file mode 100644 index 6be43e9..0000000 --- a/build/versioning.cake +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -// Do not use #addin because the assembly is distributed within Cake.Tool -#r NuGet.Versioning - -#nullable enable - -using NuGet.Versioning; - -// --------------------------------------------------------------------------------------------- -// Version management helpers -// --------------------------------------------------------------------------------------------- - -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; - -/* - * Summary : Specifies how to modify the version specification upon publishing a release. - */ -enum VersionSpecChange -{ - /* - * Summary : Do not force a version increment; do not modify the unstable tag. - */ - None, - - /* - * Summary : Do not force a version increment; add an unstable tag if not present. - */ - Unstable, - - /* - * Summary : Do not force a version increment; remove the unstable tag if present. - */ - Stable, - - /* - * Summary : Force a minor version increment with respect to the latest stable version; add an unstable tag. - */ - Minor, - - /* - * Summary : Force a major version increment and minor version reset with respect to the latest stable version; add an unstable tag. - */ - Major, -} - -/* - * Summary : Specifies a kind of version increment. - * Remarks : The values of this enum are sorted in ascending order of importance, - * so that they may be compared. - */ -enum VersionIncrement -{ - /* - * Summary : Represents no version advancement. - */ - None, - - /* - * Summary : Represents the increment of minor version. - */ - Minor, - - /* - * Summary : Represents the increment of major version and reset of minor version. - */ - Major, -} - -/* - * Summary : Represents a Major.Minor[-Tag] version as found in version.json. - */ -sealed record VersionSpec -{ - private static readonly Regex VersionSpecRegex = new Regex( - @"(?-imsx)^v?(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)(-(?.*))?$", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture); - - private VersionSpec(int major, int minor, string tag) - { - Major = major; - Minor = minor; - Tag = tag; - } - - /* - * Summary : Gets the major version. - */ - public int Major { get; } - - /* - * Summary : Gets the minor version. - */ - public int Minor { get; } - - /* - * Summary : Gets current unstable tag. - * Value : The current unstable tag, or the empty string if the current version is stable. - */ - public string Tag { get; } - - /* - * Summary : Gets a value indicating whether this instance has an unstable tag. - */ - public bool HasTag => !string.IsNullOrEmpty(Tag); - - /* - * Summary : Attempts to parse a VersionSpec from the specified string. - * Params : str - The string to parse. - * result - When this method returns true, the parsed VersionSpec. - * Returns : True is successful; false otherwise. - */ - public static bool TryParse(string str, [MaybeNullWhen(false)] out VersionSpec result) - { - var match = VersionSpecRegex.Match(str); - if (!match.Success) - { - result = null; - return false; - } - - result = new( - int.Parse(match.Groups["major"].Value), - int.Parse(match.Groups["minor"].Value), - match.Groups["tag"].Value - ); - - return true; - } - - public override string ToString() => $"{Major}.{Minor}{(HasTag ? "-" + Tag : null)}"; - - /* - * Summary : Gets an instance of VersionSpec that represents the same version as the current instance - * and has no unstable tag. - * Returns : If this instance has no unstable tag, this instance; otherwise, a newly-constructed VersionSpec - * that represents the same version as the current instance and has no unstable tag. - */ - public VersionSpec Stable() => HasTag ? new(Major, Minor, string.Empty) : this; - - /* - * Summary : Gets an instance of VersionSpec that represents the same version as the current instance - * and has the specified unstable tag. - * Params : tag - The unstable tag of the returned instance. - * Returns : If this instance's Tag property is equal to the given tag, this instance; otherwise, a newly-constructed VersionSpec - * that represents the same version as the current instance and has the specified unstable tag. - */ - public VersionSpec Unstable(string tag) => string.Equals(Tag, tag, StringComparison.Ordinal) ? this : new(Major, Minor, tag); - - /* - * Summary : Gets an instance of VersionSpec that represents the next minor version with respect to the current instance - * and has the specified unstable tag. - * Params : tag - The unstable tag of the returned instance. - * Returns : A newly-constructed VersionSpec. - */ - public VersionSpec NextMinor(string tag) => new(Major, Minor + 1, tag); - - /* - * Summary : Gets an instance of VersionSpec that represents the next major version with respect to the current instance - * and has the specified unstable tag. - * Params : tag - The unstable tag of the returned instance. - * Returns : A newly-constructed VersionSpec. - */ - public VersionSpec NextMajor(string tag) => new(Major + 1, 0, tag); - - /* - * Summary : Gets an instance of VersionSpec that represents the result of applying the specified change - * to the current instance. - * Params : action - An enumeration value representing the kind of change to apply. - * tag - If the returned instance has an unstable tag, the unstable tag of the returned instance; - * otherwise, this parameter is ignored. - * Returns : Result - The result of applying action to the current instance. - * Changed - If Result is equal to the current instance, false; otherwise, true. - */ - public (VersionSpec Result, bool Changed) ApplyChange(VersionSpecChange change, string tag) - => change switch { - VersionSpecChange.Unstable => HasTag ? (this, false) : (Unstable(tag), true), - VersionSpecChange.Stable => HasTag ? (Stable(), true) : (this, false), - VersionSpecChange.Minor => (NextMinor(tag), true), - VersionSpecChange.Major => (NextMajor(tag), true), - _ => (this, false), - }; -} - -/* - * Summary : Represents the version.json file, for the purpose of applying version advances. - */ -sealed class VersionFile -{ - private const string VersionJsonPath = "version.json"; - private const string DefaultFirstUnstableTag = "preview"; - - private readonly ICakeContext _context; - private readonly JsonNode _json; - - private VersionFile(ICakeContext context, FilePath path, JsonNode json, VersionSpec versionSpec, string firstUnstableTag) - { - _context = context; - Path = path; - _json = json; - VersionSpec = versionSpec; - FirstUnstableTag = firstUnstableTag; - } - - /* - * Summary : Gets the FilePath of the version.json file. - */ - public FilePath Path { get; } - - /* - * Summary : Gets a VersionSpec representing the "version" value in the version.json file. - */ - public VersionSpec VersionSpec { get; private set; } - - /* - * Summary : Gets the unstable tag to use for version advances. - * Value : Either the "release.firstUnstableTag" value read from version.json, - * or "preview" as a default value. - */ - public string FirstUnstableTag { get; private init; } - - /* - * Summary : Constructs a VersionFile instance by loading the repository's version.json file. - * Returns : A newly-constructed instance of VersionFile, representing the loaded data. - */ - public static VersionFile Load(ICakeContext context) - { - var path = new FilePath(VersionJsonPath); - var json = context.LoadJsonObject(path); - var versionStr = context.GetJsonPropertyValue(json, "version", path + " file"); - context.Ensure(VersionSpec.TryParse(versionStr, out var versionSpec), $"{VersionJsonPath} contains invalid version specification '{versionStr}'."); - var firstUnstableTag = DefaultFirstUnstableTag; - var release = json["release"]; - if (release is not null) - { - var firstUnstableTagNode = release["firstUnstableTag"]; - if (firstUnstableTagNode is JsonValue firstUnstableTagValue && firstUnstableTagValue.TryGetValue(out var firstUnstableTagStr) && !string.IsNullOrEmpty(firstUnstableTagStr)) - { - firstUnstableTag = firstUnstableTagStr; - } - } - - return new(context, path, json, versionSpec, firstUnstableTag); - } - - /* - * Summary : Applies a version spec change to this instance. - * Params : context - The Cake context. - * change - An enumeration value representing the kind of change to apply. - * Returns : If the VersionSpec property is actually changed as a result of change, true; otherwise, false. - * Remarks : - This method does not save the modified version.json file; you will have to call the Save method - * if this method returns true. - */ - public bool ApplyVersionSpecChange(ICakeContext context, VersionSpecChange change) - { - var previousVersionSpec = VersionSpec; - (VersionSpec, var changed) = VersionSpec.ApplyChange(change, FirstUnstableTag); - if (changed) - { - context.Information($"Version spec changed from {previousVersionSpec} to {VersionSpec}."); - } - else - { - context.Information("Version spec not changed."); - } - - return changed; - } - - /* - * Summary : Saves the version.json file, possibly with a modified VersionSpec, back to the repository. - */ - public void Save() - { - _json["version"] = JsonValue.Create(VersionSpec.ToString()); - _context.SaveJson(_json, Path); - } -} - -/* - * Summary : Checks the consistency of current version with respect to latest versions. - * Params : context - The Cake context. - * currentVersion - The current version as computed by NBGV - * latestVersion - The latest published version, if any - * latestStableVersion - The latest published stable version, if any - * isFinalCheck - true if this is the final check before publishing; - * false if current version might still be incremented, for example by updating the changelog. - */ -static void CheckVersioningConsistency( - this ICakeContext context, - SemanticVersion currentVersion, - SemanticVersion? latestVersion, - SemanticVersion? latestStableVersion, - bool isFinalCheck) -{ - context.Ensure( - VersionComparer.Compare(latestVersion, latestStableVersion, VersionComparison.Version) >= 0, - $"Versioning anomaly detected: latest version ({latestVersion?.ToString() ?? "none"}) is lower than latest stable version ({latestStableVersion?.ToString() ?? "none"})."); - if (isFinalCheck) - { - context.Ensure( - VersionComparer.Compare(currentVersion, latestStableVersion, VersionComparison.Version) > 0, - $"Versioning anomaly detected: current version ({currentVersion}) is not higher than latest stable version ({latestStableVersion?.ToString() ?? "none"})."); - context.Ensure( - VersionComparer.Compare(currentVersion, latestVersion, VersionComparison.Version) > 0, - $"Versioning anomaly detected: current version ({currentVersion}) is not higher than latest version ({latestVersion?.ToString() ?? "none"})."); - } - else - { - context.Ensure( - VersionComparer.Compare(currentVersion, latestStableVersion, VersionComparison.Version) >= 0, - $"Versioning anomaly detected: current version ({currentVersion}) is lower than latest stable version ({latestStableVersion?.ToString() ?? "none"})."); - context.Ensure( - VersionComparer.Compare(currentVersion, latestVersion, VersionComparison.Version) >= 0, - $"Versioning anomaly detected: current version ({currentVersion}) is lower than latest version ({latestVersion?.ToString() ?? "none"})."); - } -} - -/* - * Summary : Computes the VersionSpecChange to apply upon release. - * Params : context - The Cake context. - * currentVersion - The current version as computed by NBGV - * latestVersion - The latest published version, if any - * latestStableVersion - The latest published stable version, if any - * requestedChange - The version spec change requested by the user - * checkPublicApi - If true, account for changes in public API files. - * Returns : The actual change to apply . - */ -static VersionSpecChange ComputeVersionSpecChange( - this ICakeContext context, - SemanticVersion currentVersion, - SemanticVersion? latestVersion, - SemanticVersion? latestStableVersion, - VersionSpecChange requestedChange, - bool checkPublicApi) -{ - // Determine how we are currently already incrementing version - var currentVersionIncrement = latestStableVersion == null ? VersionIncrement.None - : currentVersion.Major > latestStableVersion.Major ? VersionIncrement.Major - : currentVersion.Minor > latestStableVersion.Minor ? VersionIncrement.Minor - : VersionIncrement.None; - context.Information($"Current version increment: {currentVersionIncrement}"); - - // Determine the kind of change in public API - var publicApiChangeKind = checkPublicApi ? context.GetPublicApiChangeKind() : ApiChangeKind.None; - context.Information($"Public API change kind: {publicApiChangeKind}{(checkPublicApi ? null : " (not checked)")}"); - - // Determine the version increment required by SemVer rules - // When the major version is 0, "anything MAY change" according to SemVer; - // by convention, we increment the minor version for breaking changes (0.x -> 0.(x+1)) - var isMajorVersionZero = latestStableVersion is { Major: 0 }; - var semanticVersionIncrement = publicApiChangeKind switch { - ApiChangeKind.Breaking => isMajorVersionZero ? VersionIncrement.Minor : VersionIncrement.Major, - ApiChangeKind.Additive => isMajorVersionZero ? VersionIncrement.None : VersionIncrement.Minor, - _ => VersionIncrement.None, - }; - context.Information($"Required version increment according to Semantic Versioning rules: {semanticVersionIncrement}"); - - // Determine the requested version increment, if any. - context.Information($"Requested version spec change: {requestedChange}"); - var requestedVersionIncrement = requestedChange switch { - VersionSpecChange.Major => VersionIncrement.Major, - VersionSpecChange.Minor => VersionIncrement.Minor, - _ => VersionIncrement.None, - }; - context.Information($"Requested version increment: {requestedVersionIncrement}."); - - // Adjust requested version increment to follow SemVer rules - if (semanticVersionIncrement > requestedVersionIncrement) - { - requestedVersionIncrement = semanticVersionIncrement; - } - - // Determine the kind of version increment actually required - var actualVersionIncrement = requestedVersionIncrement > currentVersionIncrement ? requestedVersionIncrement : VersionIncrement.None; - context.Information($"Required version increment with respect to current version: {actualVersionIncrement}"); - - // Determine the actual version spec change to apply: - // - forget any increment-related change (already accounted for via requestedVersionIncrement) - // - set the change to the required increment if any, otherwise leave it as is (None, Unstable, Stable) - var actualChange = requestedChange switch { - VersionSpecChange.Major or VersionSpecChange.Minor => VersionSpecChange.None, - _ => requestedChange, - }; - actualChange = actualVersionIncrement switch { - VersionIncrement.Major => VersionSpecChange.Major, - VersionIncrement.Minor => VersionSpecChange.Minor, - _ => actualChange, - }; - context.Information($"Actual version spec change: {actualChange}."); - - return actualChange; -} diff --git a/build/workspace.cake b/build/workspace.cake deleted file mode 100644 index d0885be..0000000 --- a/build/workspace.cake +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) Tenacom and contributors. Licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -#nullable enable - -// --------------------------------------------------------------------------------------------- -// Workspace helpers -// --------------------------------------------------------------------------------------------- - -/* - * Summary : Delete all intermediate and output directories. - * On a local machine, also delete Visual Studio and ReSharper caches. - * Params : context - The Cake context. - */ -static void CleanAll(this ICakeContext context, BuildData data) -{ - context.DeleteDirectoryIfExists(".vs"); - context.DeleteDirectoryIfExists("_ReSharper.Caches"); - context.DeleteDirectoryIfExists("artifacts"); - context.DeleteDirectoryIfExists("logs"); - context.DeleteDirectoryIfExists("TestResults"); - foreach (var project in data.Solution.Projects) - { - var projectDirectory = project.Path.GetDirectory(); - context.DeleteDirectoryIfExists(projectDirectory.Combine("bin")); - context.DeleteDirectoryIfExists(projectDirectory.Combine("obj")); - } -}