Building and Deploying Applications With Cake - p-patel/software-engineer-knowledge-base GitHub Wiki

Introduction

Build automation:

  • Compile - e.g. msbuild
  • Test - e.g. nunit/xunit
  • Package - e.g. nuget pack
  • Deploy - msdeploy

Make - first build automation tool (by Stuart Feldman)

What is Cake?:

Cake - written in C#, cross-platform, extensible Cake = C# + Make

Why Cake?:

dependency-tracking model (taken from Make): define tasks and their dependencies Cake will determine which dependent tasks need to be run for any specified task ANT/Maven - XML-based Make Rake/Psake/Fake - extend existing languages with a domain-specific languages (DSL) (Ruby, Powershell, F#)

  • Cake DSL: e.g. Task(), IsDependentOn(), RunTarget()
  • to go from source to running software
  • a tool that makes it easier to invoke external programs, passing parameters to them and gathering their results in a form that can be passed to other programs. Also want the too report and handle any errors from these tools in a consistent way so that you can make decisions from these results (a rich ecosystem of integrations of tools to build and deploy applications, making them available to build scripts)
  • tools are imported into build script as nuget packages from any nuget repository

Getting Started

How Cake Works

  • A Cake script is a C# program

  • The Cake Workflow:

  • invoke Cake executable with path to script to be run

  • Cake reads script, parses the script as a single string and feeds string into C# compiler "Roslyn", which runs it. A Cake script can reference any C# assembly. All Cake scripts reference at least Cake.Core.dll and Cake.Common.dll

  • instead of referencing assemblies directly, versions of nuget packages are referenced instead (Cake will download all packages on the first run)

Anatomy of a Cake Script

  • a Task accomplishes one thing
  • a sample script:

var value = Argument("Name", "DefaultValue");

Task("Name") .IsDependentOn("AnotherName") .IsDependentOn("YetAnotherName") .Does(() => { //work });

RunTarget(value);

  • Basic Aliases: Task, IsDependentOn, RunTarget, Argument

Aliases

  • .WithCriteria(...) //must evaluate to true for the associated Task to run
  • .ContinueOnError()
  • .ReportError(exception => { // error reporting });
  • .OnError(exception => { // error handling });
  • throw an exception to abort the script
  • Setup(CakeContext context => { // before the first task } ); / Teardown(CakeContext context => { // after the last task } );
  • TaskSetup(CakeContext context, CakeTask cakeTask => { // before each task } ); / TaskTeardown(CakeContext context, CakeTask cakeTask => { // after each task } );

The Context

  • a Cake script is a C# program that is compiled and run on the fly
  • The object model: ScriptHost (contains Context) -> Script
  • e.g. Context.Environment.WorkingDirectory / Context.Environment.GetEnvironmentVariables()["http_proxy"]
  • aliases are extension methods decorated with a specific Cake attribute - e.g. Verbose("some logging message") / EnvironmentVariable("http_proxy")

Demo: Compiling on Windows

  • Compiling an application using Cake
  • using MSBuild (windows) and .NET Core CLI tools (macOS)

Windows

  • use Cake extension for VS.NET to download the bootstrapper for PowerShell
  • run (empty) bootstrapper from Package Manager Console
  • create: Task("Build").Does(() => { DotNetBuild("src/Linker.sln", settings => settings.SetConfiguration("Debug") .WithTarget("Build")); });
  • add: RunTarget("Build");
  • save and run .\build.ps1
  • create: Task("Restore").Does( () => { NuGetRestore("src/Linker.sln") } );
  • make Build task dependent on Restore task
  • define: var target = Argument("Target", "Build"); and update RunTarget(target)

macOS

  • (to do...)

The Bootstrapper (build.ps1)

  • downloads nuget and cake and runs our script
  • Cake is a command line tool (distributed as a nuget package), to get this the nuget command line tool needs to be downloaded (all this is the bootstrapper's job)
  • bootstrapper is a shell script (checks/downloads Nuget command line tool and Cake tool into Tools directory)
  • bootstrapper translates parameters passed to it into the format expected by the Cake executable
  • bootstrapper parameters:
    • Script, Target, Configuration, Verbosity, Mono, Experimental, DryRun
    • can also provide values for any other parameters defined in the build script
  • Bootstrapper and Cake are separate entities (Cake is completely independent from the Bootstrapper and therefore using the Bootstrapper is optional, but is usually always used)
  • The Bootstrapper requires network access (the first time it runs)
  • The Bootstrapper is a shell script can be customised

The Configuration

  • Cake has two main responsibilities - compiling C# scripts and downloading third-party tools referenced by the scripts (the scripts are run by Roslyn)
  • Cake can be configured by environment variables, arguments or the cake.config file
  • Configuration Settings:
    • URLs: NuGet_Source, Roslyn_NuGetSource (can be network addresses or local directories)
    • Paths: where to download and unpack packages that are downloaded - Paths_Tools, Paths_Addins, Paths_Modules
  • Modifying the Boostrapper (Windows)
    • by default, if Cake is not found in the tools directory by the Bootstrapper it will download the latest stable version available.
    • for a stable build process, specify a fixed version of Cake (in tools\packages.config)
    • the default target and version of Roslyn can be customised in the bootstrapper script (.\build.ps)

Testing

  • run tests, measure test coverage (with Open Cover) and generate a report of the code coverage analysis

Preprocessor Directives

  • Aliases make it easy to interact with external command line programs
  • Bootstrapper is used to add any additional tools required by the build script (can add them to tools\packages.config or use preprocessor directives)
  • #tool and #load directives
  • #tool tells nuget to download a specific package and store in \tools directory (usually third-party tools we want to interact with)
  • e.g. #tool nuget:?package=Name&version=1.2.3 (each tool is downloaded to a subdirectory under \tools)
  • can also use custom tools that are not from a nuget package (and there is no built-in Alias). register the tool using context.Tools.RegisterFile(@"C:\Some\Program.exe"); and use the tool in a Task using var path = Context.Tools.Resolve("Program.exe"); StartProcess(path);

Referencing Other Cake Scripts

  • Build process can be split across multiple Cake scripts using the #load directive
  • e.g. #load path/to/script.cake (can also reference nuget packages, Cake will reference all finds in the package with a .cake extension) NuGet_Source can be used to source nuget packages from repositories other than nuget.org.

Splitting A Script into Multiple Files

  • Create a .\build directory
  • Create .\build\paths.cake (contains paths the various files and directories needed by the build script) and define Paths static class with static FilePath/DirectoryPath properties
  • Note: At run-time Cake will merge the build script and all the files it references into a single string
  • FilePath and DirectoryPath types for file and directory paths (provide helper functions)

Running Tests On Windows

  • add #tool nuget:?package=xunit.runner.console&version=2.2.0
  • create Task("Test").IsDependentOn("Build").Does(() => { XUnit2($"**/bin/{configuration}/*Tests.dll") });
  • run ./build.ps1 -Target Test

Running Tests On macOS

  • running on macOS uses .NET Core
  • uses high level 'dotnet test' command (uses Test Adapter to specific test runner)

Reporting Code Coverage On Windows

  • a metric that us how much of our code is covered by tests (by LOC)
  • using OpenCover to measure (Windows) and ReportGenerator to report (Windows)
  • add #tool nuget:?package=OpenCover&version=4.6.519
  • use built-in Alias to invoke the tool as part of the Build task: OpenCover() (turn off XUnit2 shadow copying!)
  • add #tool nuget:?package=ReportGenerator&version=2.5.8
  • create Task("Report-Coverage") and invoke ReportGenerator() Alias
  • create codeCoverageReportPath as an Argument so that report path can be changed depending on whether the build script is run locally or on a CI server
  • run .\build.ps1 -Target Report-Coverage
  • the generated report zip file can be published as an artefact in a CI server!

Versioning

  • Semantic Versioning (SemVer):
    • guidelines for using versioning to communicate contents of a particular software version
    • if SemVer is being adhered to, make this clear in software documentation
  • 3 components - major.minor.patch
  • HOW: only 1 component can be increased at a time all components to the right must be reset to 0
  • WHEN:
    • patch component: bug fixes in a way that is backwards-compatible
    • minor component: new features that are backwards-compatible
    • major component: any changes that are incompatible with previous releases
  • ADDITIONAL INFORMATION
    • pre-release tags (appended to version number with a hyphen) e.g. 2.0.0-alpha/2.0.0-beta/2.0.0-beta.1
    • build metadata (same as pre-release tags but separated by a plus) e.g. 2.0.0+debug
    • comparing version numbers - with a pre-release is lower than without, when two versions both have pre-release tags they are compare left-to-right (numbers numerically, letters based on ASCII sort ordering and when the rest of the tag is equal, the longest tag is the greater version

Versioning Based on History

  • derive a SemVer from the history of the source code, e.g.
    • master branch commits denote patch component increments
    • develop branch commits denote minor component increments
  • with these fixed rules it is possible to automate versioning as part of the build process

Demo: Assigning a Version on Windows

  • implement a version calculation algorithm for workflow
  • alternatively configure a third-party tool (GitVersion) to generate version numbers
  • GitVersion is configurable and integrates with many CI environments
  • GitVersion is configured in GitVersion.yml and increments version number using tagged commits in the repo
  • create Task("Version")
    • var version = GitVersion(); (and use version.SemVer / version.NuGetVersion)
    • use Version Number as the CI Build ID: if(!BuildSystem.IsLocal){ GetVersion(new GitVersionSettting{ OutputType = GitVesionOutput.BuildServer, UpdateAssemblyInfo = true }); }

Packaging

Package Formats

  • Nuget, Web Deploy / MSDeploy (for ASP.NET web apps inc. db migrations) and Zip Archive packages

Creating a NuGet package on Windows

var packageOutputPath = Argument<DirectoryPath>("PackageOutputPath", "packages");

Task("Remove-Packages")
  .Does(() => {
  CleanDirectory(packageOutputPath)
  });

Task("Package-NuGet")
  .IsDependentOn("Test")
  .IsDependentOn("Version")
  .IsDependentOn("Remove-Packages")
  .Does(() =>
  {
    EnsureDirectoryExists(packageOutputPath);
    NuGetPack(
      Paths.WebNuspecFile,  //define in paths.cake
      new NuGetPackSettings
      {
        Version = packageVersion,
        OutputDirectory = packageOutputPath,
        NoPackageAnalysis = true  //optional
      }
    );
  });

Creating a Web Deploy package on Windows

  • uses the Web Publishing Pipeline (configuration for this can be seen in project Properties 'Package\Publish Web')
  • trigger packaging and deployment of a web project by running MSBuild.exe against a cs proj file using MSBuild.exe Web.csproj /target:Package
Task("Package-WebDeploy")
  .IsDependentOn("Test")
  .IsDependentOn("Version")
  .IsDependentOn("Remove-Packages")
  .Does(() =>
  {
    EnsureDirectoryExists(packageOutputPath);
    packagePath = MakeAbsolute(packageOutputPath).CombineWithFilePath($"Linker.{packageVersion}.zip")

    MSBuild(
    Paths.WebProjectFile,  // define in paths.cake
    settings => settings.SetConfiguration(configuration)
        .WithTarget("Package")
        .WithProperty("PackageLocation", packagePath.FullPath)
    );
  });

Deploying

Deployment Tools

  • Nuget -> Octopus Deploy
  • Web Deploy -> MS Web Deploy tool
  • Zip Archive -> uploading directly using CURL + Kudu

Extending Cake with Addins

  • Aliases link Cake to external tools
  • Aliases loaded from dlls
  • Addins are .NET assemblies that contain Aliases as public static method decorated with a particular Cake attribute
  • Addins can be referenced in Cake scripts in the same way as tools or other Cake scripts (by using the #addin pre-processor directive)
  • the only supported Addin package source is Nuget - e.g. #addin nuget:?package=Name&version=1.2.3
  • Cake will download addin from specified repository and save to a \Addins sub-directory of \Tools
  • All found assemblies will be loaded into Cake runtime and made available to the Cake script
  • Addins may require a command-line tool, external library or web api
  • Addins available - https://cakebuild.net/addins/

Demo: Overview of Octopus Deploy

  • Octopus Deploy manages - environments, environment-specific settings, version numbers, packages, log etc.
  • Process - the steps to deploy the project
  • Project variables can be used to configure project specific details such as the SiteName which can be made to vary by Environment
  • Octopus Deploy operates as a package repository (Library section)
    • Octopus Deploy maintains a link between packages and the releases in which they have been deployed and to which environment

Demo: Deploying to Azure with Octopus Deploy on Windows

  • use #tool nuget:?package=OctopusTools&version=4.21.0
  • create deployment task
Task("Deploy-OctopusDeploy")
  .IsDependentOn("Package-NuGet")
  .Does(() => {
    OctoPush(
      Urls.OctopusServerUrl,  //define in build/urls.cake
      EnvironmentVariable("OctopusApiKey"),
      GetFiles($"{packageOutputPath}/*.nupkg"),
      new OctopusPushSettings
      {
        ReplaceExisting = true  //for auto-overwriting package in Octopus Deploy during script testing, remove in Production!!!
      }
      );

    OctoCreateRelease(  //combines 'Release' and 'Deployment' steps
      "Linker",  //Octopus project name
      new CreateReleaseSettigs
      {
        Server = Urls.OctopusServerUrl,
        ApiKey = EnvironmentVariable("OctopusApiKey"),
        ReleaseNumber = packageVersion,
        DefaultPackageVersion = packageVersion,
        DeployTo = "Test",        //instructs deployment to take place
        WaitForDeployment = true  //waits for async deployment process result so it can be checked/reported
      }
    );
  });
  • run deployment Task:
> $env:OctopusApiKey = "123XYZ"
> .\build.ps1 -Target Deploy-OctopusDeploy
  • view deployment status and logs on Octopus Deploy Dashboard

Continuous Integration

CI vs CD

  • get source code from repo server, compile source code, run tests, assigning version number, creating a deployment package
  • process definition is usually stored in e.g. XML as part of the CI server. Disadvantages are:
  1. can't be run outside of the server
  2. most CI's don't keep a journal of changes to the build process definition so changes cannot be tracked
  3. every server has it's way to define the build process so it cannot be easily moved to another CI server
  • a solution is to implement build process independently from the CI server. CI server then simply runs the build process the same way as it is run locally and as the build process is just a file in the project it is version controlled
  • CI process can be extended with a further step which automates the deployment an environment - this is where CI evolves into CD
  • CD requires that software is always in a state where it can be deployed at a moment's notice and requires that the deployment process is automated and triggered by the push of a button
  • Continuous Delivery is distinct from Continuous Deployment (Continuous Deployment is where software is automatically deployed to production with every build)

Demo: Building and Deploying in TeamCity

⚠️ **GitHub.com Fallback** ⚠️