Skip to content

Commit

Permalink
Refactor versioning in build / release scripts.
Browse files Browse the repository at this point in the history
Signed-off-by: Riccardo De Agostini <[email protected]>
  • Loading branch information
rdeago committed Nov 23, 2022
1 parent c7d113b commit 7f5d68f
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 313 deletions.
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ concurrency:
on:
workflow_dispatch:
inputs:
versionAdvance:
description: 'Version advance'
versionSpecChange:
description: 'Version spec change'
required: true
type: choice
default: 'None'
Expand Down Expand Up @@ -50,18 +50,18 @@ jobs:
PRERELEASE_NUGET_KEY: ${{ secrets.MYGET_DEPLOYMENT_KEY }}
RELEASE_NUGET_SOURCE: 'https://api.nuget.org/v3/index.json'
RELEASE_NUGET_KEY: ${{ secrets.NUGET_DEPLOYMENT_KEY }}
VERSION_ADVANCE: ${{ inputs.versionAdvance }}
VERSION_SPEC_CHANGE: ${{ inputs.versionSpecChange }}
CHECK_PUBLIC_API: ${{ inputs.checkPublicApi }}
CHECK_CHANGELOG: ${{ inputs.checkChangelog }}
CAKE_VERBOSITY: ${{ inputs.cakeVerbosity }}
steps:
- name: Log workflow inputs
shell: cmd
run: |
echo Version advance : %VERSION_ADVANCE%
echo Check public API : %CHECK_PUBLIC_API%
echo Check changelog : %CHECK_CHANGELOG%
echo Cake verbosity : %CAKE_VERBOSITY%
echo Version spec change : %VERSION_SPEC_CHANGE%
echo Check public API : %CHECK_PUBLIC_API%
echo Check changelog : %CHECK_CHANGELOG%
echo Cake verbosity : %CAKE_VERBOSITY%
- name: Checkout repository with full history
uses: actions/checkout@v3
with:
Expand Down
46 changes: 18 additions & 28 deletions build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#load "./build/process.cake"
#load "./build/public-api.cake"
#load "./build/setup-teardown.cake"
#load "./build/version.cake"
#load "./build/versioning.cake"
#load "./build/workspace.cake"

#nullable enable
Expand Down Expand Up @@ -70,10 +70,17 @@ Task("Release")
.Description("Publish a new public release (CI only)")
.Does<BuildData>(async (context, data) => {
// Preliminary checks
// Perform some preliminary checks
Ensure(data.IsCI, "The Release target cannot run on a local system.");
Ensure(data.IsPublicRelease, "Cannot create a release from the current branch.");
// 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,
requestedChange: context.GetOption<VersionSpecChange>("versionSpecChange", VersionSpecChange.None),
checkPublicApi: context.GetOption<bool>("checkPublicApi", true));
// Identify Git user for later push if needed
context.GitSetUserIdentity("Buildvana", "[email protected]");
Expand All @@ -83,36 +90,18 @@ Task("Release")
var committed = false;
try
{
// Advance version if requested.
var versionAdvance = context.GetOption<VersionAdvance>("versionAdvance", VersionAdvance.None);
if (context.GetOption<bool>("checkPublicApi", true))
// Modify version if required.
if (versionSpecChange != VersionSpecChange.None)
{
var requiredVersionAdvance = context.GetMaxPublicApiRequiredVersionAdvance();
Ensure(versionAdvance >= requiredVersionAdvance, $"Changes to public API require a minimum version advance of {requiredVersionAdvance}.");
}
if (versionAdvance != VersionAdvance.None)
{
context.Information($"Version advance requested: {versionAdvance}.");
var versionFile = VersionFile.Load();
var previousVersionSpec = versionFile.VersionSpec;
if (versionFile.AdvanceVersion(versionAdvance))
if (versionFile.ApplyVersionSpecChange(context, versionSpecChange))
{
context.Information($"Version advanced from {previousVersionSpec} to {versionFile.VersionSpec}.");
versionFile.Save();
UpdateRepo(versionFile.Path);
}
else
{
context.Information("Version not changed.");
}
}
else
{
context.Information("No version advance requested.");
}
// Update public API files only on non-prerelease
// Update public API files only when releasing a stable version
if (!data.IsPrerelease)
{
var modified = context.TransferAllPublicApiToShipped().ToArray();
Expand Down Expand Up @@ -160,7 +149,7 @@ Task("Release")
// 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.
Ensure(!context.GitTagExists(data.Version), $"Tag {data.Version} already exists in repository.");
Ensure(!context.GitTagExists(data.VersionStr), $"Tag {data.VersionStr} already exists in repository.");
dupeTagChecked = true;
context.RestoreSolution(data);
Expand Down Expand Up @@ -219,13 +208,13 @@ Task("Release")
// Set outputs for subsequent steps in GitHub Actions
if (data.IsGitHubAction)
{
context.SetActionsStepOutput("version", data.Version);
context.SetActionsStepOutput("version", data.VersionStr);
}
}
catch (Exception e)
{
context.Error(e is CakeException ? e.Message : $"{e.GetType().Name}: {e.Message}");
await context.DeleteReleaseAsync(data, releaseId, dupeTagChecked ? data.Version : null);
await context.DeleteReleaseAsync(data, releaseId, dupeTagChecked ? data.VersionStr : null);
throw;
}
Expand Down Expand Up @@ -253,14 +242,15 @@ Task("Release")
// 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.Version} [skip ci]"));
.AppendQuoted($"Prepare release {data.VersionStr} [skip ci]"));
committed = true;
}
Expand Down
18 changes: 14 additions & 4 deletions build/BuildData.cake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#nullable enable

using NuGet.Versioning;

// ---------------------------------------------------------------------------------------------
// BuildData: a record to hold build configuration data
// ---------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -37,7 +39,8 @@ sealed class BuildData
|| context.HasEnvironmentVariable("TEAMCITY_VERSION")
|| context.HasEnvironmentVariable("JENKINS_URL");

var (version, @ref, isPublicRelease, isPrerelease) = context.GetVersionInformation();
var (versionStr, @ref, isPublicRelease, isPrerelease) = context.GetVersionInformation();
var version = SemanticVersion.Parse(versionStr);
var branch = context.GetCurrentGitBranch();
var msBuildSettings = new DotNetMSBuildSettings {
MaxCpuCount = 1,
Expand All @@ -57,6 +60,7 @@ sealed class BuildData
SolutionPath = solutionPath;
Solution = solution;
Configuration = configuration;
VersionStr = versionStr;
Version = version;
IsPublicRelease = isPublicRelease;
IsPrerelease = isPrerelease;
Expand Down Expand Up @@ -138,9 +142,14 @@ sealed class BuildData
public string Configuration { get; }

/*
* Summary : Gets the version to build, as computed by Nerdbank.GitVersioning.
* 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 string Version { get; private set; }
public SemanticVersion Version { get; private set; }

/*
* Summary : Gets a value that indicates whether a public release can be built.
Expand Down Expand Up @@ -175,7 +184,8 @@ sealed class BuildData
*/
public void Update(ICakeContext context)
{
(Version, Ref, IsPublicRelease, IsPrerelease) = context.GetVersionInformation();
(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}");
Expand Down
2 changes: 1 addition & 1 deletion build/changelog.cake
Original file line number Diff line number Diff line change
Expand Up @@ -262,5 +262,5 @@ static void UpdateChangelogNewSectionTitle(this ICakeContext context, BuildData

static string MakeChangelogSectionTitle(BuildData data)
{
return $"[{data.Version}](https://github.com/{data.RepositoryOwner}/{data.RepositoryName}/releases/tag/{data.Version}) ({DateTime.Now:yyyy-MM-dd})";
return $"[{data.VersionStr}](https://github.com/{data.RepositoryOwner}/{data.RepositoryName}/releases/tag/{data.VersionStr}) ({DateTime.Now:yyyy-MM-dd})";
}
41 changes: 41 additions & 0 deletions build/git.cake
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,47 @@ static bool TryGetRepositoryInfo(this ICakeContext context, out (string Remote,
*/
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? version = null;
var result = SemanticVersion.TryParse(x, out version);
return version;
})
.Where(static x => x != null);

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.
Expand Down
4 changes: 2 additions & 2 deletions build/github.cake
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ using SysFile = System.IO.File;
*/
static async Task<int> CreateDraftReleaseAsync(this ICakeContext context, BuildData data)
{
var tag = data.Version;
var tag = data.VersionStr;
var client = context.CreateGitHubClient();
context.Information($"Creating a provisional draft release...");
var newRelease = new NewRelease(tag)
Expand All @@ -47,7 +47,7 @@ static async Task<int> CreateDraftReleaseAsync(this ICakeContext context, BuildD
*/
static async Task PublishReleaseAsync(this ICakeContext context, BuildData data, int id)
{
var tag = data.Version;
var tag = data.VersionStr;
var client = context.CreateGitHubClient();
context.Information($"Generating release notes for {tag}...");
var releaseNotesRequest = new GenerateReleaseNotesRequest(tag)
Expand Down
6 changes: 3 additions & 3 deletions build/nbgv.cake
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ using System.Text.Json.Nodes;
/*
* Summary : Gets version information using the NBGV tool.
* Params : context - The Cake context.
* Returns : Version - The project version.
* 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 Version, string Ref, bool IsPublicRelease, bool IsPrerelease) GetVersionInformation(this ICakeContext context)
static (string VersionStr, string Ref, bool IsPublicRelease, bool IsPrerelease) GetVersionInformation(this ICakeContext context)
{
var nbgvOutput = new StringBuilder();
context.DotNetTool(
Expand All @@ -34,7 +34,7 @@ static (string Version, string Ref, bool IsPublicRelease, bool IsPrerelease) Get

var json = ParseJsonObject(nbgvOutput.ToString(), "The output of nbgv");
return (
Version: GetJsonPropertyValue<string>(json, "NuGetPackageVersion", "the output of nbgv"),
VersionStr: GetJsonPropertyValue<string>(json, "NuGetPackageVersion", "the output of nbgv"),
Ref: GetJsonPropertyValue<string>(json, "BuildingRef", "the output of nbgv"),
IsPublicRelease: GetJsonPropertyValue<bool>(json, "PublicRelease", "the output of nbgv"),
IsPrerelease: !string.IsNullOrEmpty(GetJsonPropertyValue<string>(json, "PrereleaseVersion", "the output of nbgv")));
Expand Down
63 changes: 43 additions & 20 deletions build/public-api.cake
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,52 @@ using System.Text;
using SysFile = System.IO.File;

/*
* Summary : Gets the maximum required version advance, according to the presence of new public APIs
* 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, VersionAdvance.Major;
* if no public API was removed, but at least one was added, VersionAdvance.Minor;
* if no public API was removed nor added, VersionAdvance.None.
* 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 VersionAdvance GetMaxPublicApiRequiredVersionAdvance(this ICakeContext context)
static ApiChangeKind GetPublicApiChangeKind(this ICakeContext context)
{
context.Information("Computing required version advance according to unshipped public API files...");
var result = VersionAdvance.None;
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 advance = context.GetPublicApiRequiredVersionAdvance(unshippedPath);
context.Verbose($"{unshippedPath} -> {advance}");
if (advance == VersionAdvance.Major)
var fileResult = context.GetPublicApiChangeKind(unshippedPath);
context.Verbose($"{unshippedPath} -> {fileResult}");
if (fileResult == ApiChangeKind.Breaking)
{
return VersionAdvance.Major;
return ApiChangeKind.Breaking;
}
else if (advance > result)
else if (fileResult > result)
{
result = advance;
result = fileResult;
}
}

Expand Down Expand Up @@ -84,15 +107,15 @@ static IEnumerable<(FilePath UnshippedPath, FilePath ShippedPath)> GetAllPublicA
}

/*
* Summary : Gets the required version advance, according to the presence of new public APIs
* 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, VersionAdvance.Major;
* if no public API was removed, but at least one was added, VersionAdvance.Minor;
* if no public API was removed nor added, VersionAdvance.None.
* 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 VersionAdvance GetPublicApiRequiredVersionAdvance(this ICakeContext context, FilePath unshippedPath)
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] == '#';
Expand All @@ -103,13 +126,13 @@ static VersionAdvance GetPublicApiRequiredVersionAdvance(this ICakeContext conte
{
if (line.StartsWith(RemovedPrefix, StringComparison.Ordinal))
{
return VersionAdvance.Major;
return ApiChangeKind.Breaking;
}

newApiPresent = true;
}

return newApiPresent ? VersionAdvance.Minor : VersionAdvance.None;
return newApiPresent ? ApiChangeKind.Additive : ApiChangeKind.None;
}

/*
Expand Down
Loading

0 comments on commit 7f5d68f

Please sign in to comment.