Skip to content

Instantly share code, notes, and snippets.

@michaelkc
Created October 15, 2019 14:09
Show Gist options
  • Save michaelkc/72d3a8db025fea0ae3ce2b28f59328d4 to your computer and use it in GitHub Desktop.
Save michaelkc/72d3a8db025fea0ae3ce2b28f59328d4 to your computer and use it in GitHub Desktop.
A real-world example of using CAKE, TeamCity and Octopus together
#tool "nuget:?package=NUnit.ConsoleRunner&version=3.10.0"
#tool "nuget:?package=NUnit.Extension.TeamCityEventListener&version=1.0.6"
#tool "nuget:?package=JetBrains.dotCover.CommandLineTools&version=2019.2.1"
#tool "nuget:?package=OctopusTools&version=6.12.0"
#addin "nuget:?package=Octopus.Client&version=7.0.4"
#addin "nuget:?package=Newtonsoft.Json&version=12.0.2"
#addin "nuget:?package=Cake.Json&version=4.0.0"
#addin "nuget:?package=Cake.Git&version=0.21.0"
#addin "nuget:?package=Cake.FileHelpers&version=3.2.1"
#addin "nuget:?package=Cake.Http&version=0.7.0"
// Sadly, all these addin declarations are currently needed for Cake.ExtendedNuget
#addin "nuget:?package=NuGet.Common&version=5.0.2"
#addin "nuget:?package=NuGet.Configuration&version=5.0.2"
#addin "nuget:?package=NuGet.Packaging&version=5.0.2"
#addin "nuget:?package=NuGet.Versioning&version=5.0.2"
#addin "nuget:?package=NuGet.Frameworks&version=5.0.2"
#addin "nuget:?package=NuGet.Protocol&version=5.0.2"
#addin "nuget:?package=Cake.ExtendedNuGet&version=2.1.1"
Setup<BuildContext>(setupContext =>
{
BuildContext.Instance = new BuildContext(
buildCounter: EnvironmentVariable("BuildCounter") ?? "0",
versionMajor: 1, // Increment these as needed, e.g. semver 2.0 for components, a more "pragmatic" scheme for applications
versionMinor: 1,
configuration:Argument("configuration", "Release"),
octoExePath: "./tools/OctopusTools.6.12.0/tools/Octo.exe",
octopusApiKey: EnvironmentVariable("OctopusApiKey"),
octopusProjectName: "My.Octopus.Project",
octopusUrl: "https://octopus.mycompany.tld/"
);
return BuildContext.Instance;
});
Task(Tasks.UpdateAssemblyInfo)
.WithCriteria(BuildSystem.IsRunningOnTeamCity)
.IsDependentOn(Tasks.SetVersions)
.Does<BuildContext>(buildContext =>
{
ReplaceRegexInFiles( "./**/AssemblyInfo.cs",
"(?<=AssemblyVersion\\(\")(.+?)(?=\"\\))",
buildContext.AssemblySemVer);
ReplaceRegexInFiles( "./**/AssemblyInfo.cs",
"(?<=AssemblyFileVersion\\(\")(.+?)(?=\"\\))",
buildContext.AssemblySemFileVer);
ReplaceRegexInFiles( "./**/AssemblyInfo.cs",
"(?<=AssemblyInformationalVersion\\(\")(.+?)(?=\"\\))",
buildContext.InformationVersion);
});
Task(Tasks.SetVersions)
.Does<BuildContext>(buildContext =>
{
var current = GitBranchCurrent(".");
var sha = current.Tip.Sha;
var branchName = System.Text.RegularExpressions.Regex.Replace(current.FriendlyName,@"[^0-9A-Za-z-]","");
var sourceRevision = sha.Substring(0,7);
Information($"Branchname {branchName}");
buildContext.IsMasterBranch = StringComparer.OrdinalIgnoreCase.Equals("master", branchName);
Information($"Is master branch? {buildContext.IsMasterBranch.ToString()}");
var prerelease = buildContext.IsMasterBranch ? "" : "-pre"; // https://help.octopus.com/t/release-changes/23784
buildContext.TeamCityBuildNumber = $"{buildContext.VersionMajor}.{buildContext.VersionMinor}.{buildContext.BuildCounter}{prerelease}+{branchName}.{sourceRevision}";
buildContext.OctopusReleaseVersion = $"{buildContext.VersionMajor}.{buildContext.VersionMinor}.{buildContext.BuildCounter}{prerelease}+{branchName}.{sourceRevision}";
if (BuildSystem.IsRunningOnTeamCity)
{
Information("Setting TeamCity version");
TeamCity.SetBuildNumber(buildContext.TeamCityBuildNumber);
}
Information("Setting NuGet and Assembly Build Version");
buildContext.NuGetVersion = $"{buildContext.VersionMajor}.{buildContext.VersionMinor}.{buildContext.BuildCounter}{prerelease}+{branchName}.{sourceRevision}";
buildContext.InformationVersion = $"{buildContext.VersionMajor}.{buildContext.VersionMinor}.{buildContext.BuildCounter}{prerelease}+{branchName}.{sourceRevision}.sha.{sha}";
buildContext.AssemblySemVer = $"{buildContext.VersionMajor}.{buildContext.VersionMinor}.{buildContext.BuildCounter}.0";
buildContext.AssemblySemFileVer = $"{buildContext.VersionMajor}.{buildContext.VersionMinor}.{buildContext.BuildCounter}.0";
});
Task(Tasks.Build)
.IsDependentOn(Tasks.UpdateAssemblyInfo)
.Does<BuildContext>(buildContext =>
{
var solution = GetFiles(buildContext.Src + "/*.sln").First().FullPath;
DotNetCoreRestore(buildContext.Src.FullPath, new DotNetCoreRestoreSettings
{
Sources = new[] {
"https://api.nuget.org/v3/index.json",
"https://packages.mycompany.tld/nuget/dotnet/"},
});
DotNetCoreBuild(solution, new DotNetCoreBuildSettings
{
Configuration = buildContext.Configuration
});
});
Task(Tasks.Package)
.IsDependentOn(Tasks.Build)
.IsDependentOn(Tasks.SetVersions)
.Does<BuildContext>(buildContext =>
{
var webPublishDir = buildContext.Tmp + "/Web/";
CleanDirectory(webPublishDir);
CleanDirectory(buildContext.Artifacts);
var nuGetPackSettings =
new NuGetPackSettings
{
Description = buildContext.InformationVersion,
Version = buildContext.NuGetVersion,
Symbols = false,
NoPackageAnalysis = true,
Copyright = $"MyCompany {DateTimeOffset.Now.Year}",
Authors = new []{"MyCompany"},
OutputDirectory = buildContext.Artifacts,
};
DotNetCorePublish(buildContext.Src + "/Web/Web.csproj", new DotNetCorePublishSettings {
Configuration = buildContext.Configuration,
OutputDirectory = webPublishDir
});
OctoPack("MyCompany.MyApp.Web", new OctopusPackSettings{
BasePath = webPublishDir,
OutFolder = buildContext.Artifacts,
Version = buildContext.NuGetVersion
});
OctoPack("MyCompany.MyApp.Db", new OctopusPackSettings{
BasePath = buildContext.Src + $"/Db/bin/{buildContext.Configuration}/netcoreapp3.0/",
OutFolder = buildContext.Artifacts,
Version = buildContext.NuGetVersion
});
});
Task(Tasks.BuildVerification)
.IsDependentOn(Tasks.Build)
.Does<BuildContext>(buildContext =>
{
string testAssemblyPattern = buildContext.Src + $"/**/bin/{buildContext.Configuration}/**/*Tests.dll";
var testAssemblies = GetFiles(testAssemblyPattern);
foreach (var testAssembly in testAssemblies)
{
var hash = CalculateFileHash(testAssembly,HashAlgorithm.SHA256).ToHex();
DotCoverCover((ICakeContext c) =>
{
c.DotNetCoreVSTest(testAssembly.FullPath, new DotNetCoreVSTestSettings
{
TestCaseFilter = "TestCategory=BuildVerification",
Logger = TeamCity.IsRunningOnTeamCity ? "teamcity" : null,
Parallel = false
});
},
buildContext.Tmp + $"/dotcover_{hash}.dcvr",
new DotCoverCoverSettings()
.WithFilter("+:MyCompany.*")
.WithAttributeFilter("System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute"));
}
var mergedDotCoverFile = File(buildContext.Artifacts + $"/dotcover_{buildContext.NuGetVersion}.dcvr");
Information(buildContext.Tmp + "/*.dcvr");
var dotCoverFiles = GetFiles(buildContext.Tmp + "/*.dcvr");
if (!dotCoverFiles.Any())
{
return;
}
DotCoverMerge(dotCoverFiles, mergedDotCoverFile);
if(TeamCity.IsRunningOnTeamCity)
{
TeamCity.ImportDotCoverCoverage(
mergedDotCoverFile,
MakeAbsolute(Context.Tools.Resolve("dotcover.exe").GetDirectory()));
}
});
Task(Tasks.PatchOctopusMetadata)
.WithCriteria<BuildContext>((_, buildContext) => ShouldTrackBuildMetadata(buildContext))
.IsDependentOn(Tasks.SetVersions)
.Does<BuildContext>(buildContext =>
{
// Until Octopus produces code to create octopus.metadata from CAKE, we rely on a TeamCity
// step to create octopus.metadata with correct values EXCEPT BuildNumber, which we patch here
Information("Patching metadata");
var metadataFile = (FilePath)File("./octopus.metadata");
var metadataJson = System.IO.File.ReadAllText(metadataFile.FullPath);
var converter = new Newtonsoft.Json.Converters.ExpandoObjectConverter();
dynamic metadata = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Dynamic.ExpandoObject>(metadataJson, converter);
metadata.BuildNumber = buildContext.TeamCityBuildNumber;
string patchedMetadataJson = Newtonsoft.Json.JsonConvert.SerializeObject(metadata, Newtonsoft.Json.Formatting.Indented);
Information("Patched octopus.metadata:");
Information(patchedMetadataJson);
System.IO.File.WriteAllText(metadataFile.FullPath, patchedMetadataJson);
});
Task(Tasks.PublishNugetPackagesToOctopus)
.WithCriteria<BuildContext>((_, buildContext) => ShouldDeployBuild(buildContext))
.IsDependentOn(Tasks.Package)
.IsDependentOn(Tasks.BuildVerification)
.Does<BuildContext>(buildContext =>
{
foreach (var package in GetFiles(buildContext.Artifacts + "/*.nupkg"))
{
NuGetPush(package.FullPath, new NuGetPushSettings
{
ApiKey = buildContext.OctopusApiKey,
Source = $"{buildContext.OctopusUrl}nuget/packages"
});
}
});
Task(Tasks.PublishMetadataToOctopus)
.WithCriteria<BuildContext>((_, buildContext) => ShouldTrackBuildMetadata(buildContext))
.IsDependentOn(Tasks.PatchOctopusMetadata)
.IsDependentOn(Tasks.PublishNugetPackagesToOctopus)
.Does<BuildContext>(buildContext =>
{
foreach (var package in GetFiles(buildContext.Artifacts + "/*.nupkg"))
{
// The metadata step will have already uploaded metadata with the incorrect version, so we must force update it here
var packageId = GetNuGetPackageId(package.FullPath);
//var packageVersion = GetNuGetPackageVersion(package.FullPath);
var packageVersion = buildContext.NuGetVersion;
var arguments = $"push-metadata --debug --server {buildContext.OctopusUrl}api --apikey {buildContext.OctopusApiKey} --package-id {packageId} --version {packageVersion} --enableServiceMessages --overwrite-mode OverwriteExisting --metadata-file ./octopus.metadata";
Information($"push-metadata --debug --server {buildContext.OctopusUrl}api --apikey <redacted> --package-id {packageId} --version {packageVersion} --enableServiceMessages --overwrite-mode OverwriteExisting --metadata-file ./octopus.metadata");
var exitCode = StartProcess(buildContext.OctoExePath, new ProcessSettings{ Arguments = arguments });
if (exitCode != 0) throw new Exception("Octopus Metadata Push failed");
}
});
Task(Tasks.CreateOctopusRelease)
.WithCriteria<BuildContext>((_, buildContext) => ShouldDeployBuild(buildContext))
.IsDependentOn(Tasks.PublishNugetPackagesToOctopus)
.IsDependentOn(Tasks.PublishMetadataToOctopus)
.Does<BuildContext>(buildContext =>
{
OctoCreateRelease(buildContext.OctopusProjectName, new CreateReleaseSettings
{
ToolPath = buildContext.OctoExePath,
DeploymentProgress = true,
ShowProgress = true,
DeployTo = "DEV",
PackagesFolder = buildContext.Artifacts.FullPath,
ApiKey = buildContext.OctopusApiKey,
Server = $"{buildContext.OctopusUrl}api",
EnableServiceMessages = true,
ReleaseNumber = buildContext.OctopusReleaseVersion
});
});
Task(Tasks.DeploymentVerification)
.WithCriteria<BuildContext>((_, buildContext) => ShouldDeployBuild(buildContext))
.IsDependentOn(Tasks.CreateOctopusRelease)
.Does<BuildContext>(buildContext =>
{
string testAssemblyPattern = buildContext.Src + $"/**/bin/{buildContext.Configuration}/**/*Tests.dll";
var testAssemblies = GetFiles(testAssemblyPattern);
foreach (var testAssembly in testAssemblies)
{
DotNetCoreVSTest(testAssembly.FullPath, new DotNetCoreVSTestSettings
{
TestCaseFilter = "TestCategory=DeploymentVerification",
Logger = TeamCity.IsRunningOnTeamCity ? "teamcity" : null,
Parallel = false
});
}
})
.OnError(exception =>
{
var buildContext = BuildContext.Instance;
if (buildContext.OctopusApiKey != null)
{
var endpoint = new Octopus.Client.OctopusServerEndpoint(buildContext.OctopusUrl, buildContext.OctopusApiKey);
var repository = new Octopus.Client.OctopusRepository(endpoint);
var project = repository.Projects.FindByName(buildContext.OctopusProjectName);
var release = repository.Projects.GetReleaseByVersion(project, buildContext.OctopusReleaseVersion);
var releaseId = release.Id;
var deployment = repository.Deployments.FindOne(d => d.ReleaseId == releaseId);
var task = repository.Tasks.Get(deployment.TaskId);
Information("Marking deployment as failed");
repository.Tasks.ModifyState(task, Octopus.Client.Model.TaskState.Failed, "Build Verification failure");
// Blocking a release (defects api) is not currently supported via Octopus.Client or Octo.exe
// See https://help.octopusdeploy.com/discussions/questions/4096-using-the-command-line-to-block-a-release-from-progressing-down-the-life-cycle
Information("Blocking deployment from progression");
string responseBody = HttpPost($"{buildContext.OctopusUrl}api/releases/{releaseId}/defects", settings =>
{
settings
.SetContentType("appliication/json")
.AppendHeader("X-Octopus-ApiKey",buildContext.OctopusApiKey)
.SetRequestBody(SerializeJsonPretty(new { description="Build Verification failure" }));
});
}
throw exception;
});
Task(Tasks.Default)
.IsDependentOn(Tasks.DeploymentVerification);
var target = Argument("target", Tasks.Default);
RunTarget(target);
// Test locally by setting environment variables, e.g. $env:OctopusApiKey = "API-..."
bool ShouldDeployBuild(BuildContext context) => context.OctopusApiKey != null;
bool ShouldTrackBuildMetadata(BuildContext context) => System.IO.File.Exists("./octopus.metadata");
public class BuildContext
{
public BuildContext(string buildCounter, uint versionMajor, uint versionMinor, string configuration, string octoExePath, string octopusApiKey, string octopusProjectName, string octopusUrl)
{
BuildCounter = buildCounter;
VersionMajor = versionMajor;
VersionMinor = versionMinor;
Configuration = configuration;
OctoExePath = octoExePath;
OctopusApiKey = octopusApiKey;
OctopusProjectName = octopusProjectName;
OctopusUrl = octopusUrl;
}
// This is only used by OnError, which does not support typed context yet.
public static BuildContext Instance {get;set;}
public DirectoryPath Tmp {get;} = "./tmp";
public DirectoryPath Src {get;} = "./src";
public DirectoryPath Artifacts {get;} = "./artifacts";
public string BuildCounter { get; }
public uint VersionMajor { get; }
public uint VersionMinor { get; }
public string Configuration { get; }
public string OctoExePath { get; }
public string OctopusApiKey { get; }
public string OctopusProjectName { get; }
public string OctopusUrl { get; }
public bool IsMasterBranch { get; set; } = false;
public string PackageName { get; set; }
public string TargetFramework { get; }
public string AssemblySemFileVer { get; set; } = string.Empty;
public string AssemblySemVer { get; set; } = string.Empty;
public string InformationVersion { get; set; } = string.Empty;
public string NuGetVersion { get; set; } = string.Empty;
public string TeamCityBuildNumber { get; set; } = string.Empty;
public string OctopusReleaseVersion { get; set; } = string.Empty;
public string SourceRevision { get; set;} = string.Empty;
}
public static class Tasks
{
public const string Default = nameof(Default);
public const string Build = nameof(Build);
public const string UpdateAssemblyInfo = nameof(UpdateAssemblyInfo);
public const string BuildVerification= nameof(BuildVerification);
public const string Package= nameof(Package);
public const string SetVersions= nameof(SetVersions);
public const string PatchOctopusMetadata = nameof(PatchOctopusMetadata);
public const string DeploymentVerification = nameof(DeploymentVerification);
public const string CreateOctopusRelease = nameof(CreateOctopusRelease);
public const string PublishNugetPackagesToOctopus = nameof(PublishNugetPackagesToOctopus);
public const string PublishMetadataToOctopus = nameof(PublishMetadataToOctopus);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment