-
-
Save c17r/24a938853ec78d66979b80a21b8e5719 to your computer and use it in GitHub Desktop.
A real-world example of using CAKE, TeamCity and Octopus together
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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