{"id":2878,"date":"2019-09-12T14:44:35","date_gmt":"2019-09-12T02:44:35","guid":{"rendered":"https:\/\/www.deltics.co.nz\/blog\/?p=2878"},"modified":"2019-09-12T16:41:08","modified_gmt":"2019-09-12T04:41:08","slug":"azure-devops-and-delphi-template-for-builds-running-tests-and-capturing-results","status":"publish","type":"post","link":"https:\/\/www.deltics.co.nz\/blog\/posts\/2878\/","title":{"rendered":"Azure DevOps &#8211; Template for Builds + Running Tests and Capturing Results"},"content":{"rendered":"<span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">[Estimated Reading Time: <\/span> <span class=\"rt-time\">9<\/span> <span class=\"rt-label rt-postfix\">minutes]<\/span><\/span>\n<p>Last time we established that Azure DevOps is able to build Delphi code on my self-hosted build agent, but the script we used was pretty basic.  Time to look at a more sophisticated script and use pipeline templates to re-use that script where-ever I need to build a Delphi project.  We&#8217;ll also look at running tests and capturing results for presentation by Azure DevOps.<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>Before jumping into the template itself, let&#8217;s first take a look at the way we use a template in a consuming pipeline:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">trigger:\n- develop\n- master\n\npool:\n  name: &#039;The Den&#039;\n  demands: &#039;Delphi&#039;\n\n- powershell: |\n    $GitVersion = GitVersion | ConvertFrom-Json\n    Write-Host &quot;##vso[task.setvariable variable=version]$($GitVersion.SemVer)&quot;\n    Write-Host &quot;##vso[build.updatebuildnumber]$($GitVersion.SemVer)&quot;\n  displayName: Determine version for build\n\n- script: |\n    SET USES=.\\fastmm-4.99.1\n    SET USES=%USES%;..\\..\\src\n    SET USES=%USES%;.\\deltics.rtl\n    SET USES=%USES%;.\\deltics.inc\n    ECHO ##vso[task.setvariable variable=uses]%USES%\n  displayName: &#039;Initial setup for building and running tests&#039;\n\n- template: templates\/delphi-build.yml\n  parameters:\n    delphiVersion: 7\n    project: tests\\projects\\selftestD7\n    searchPath: $(uses)\n    postBuild:\n      - script: tests\\projects\\.bin\\selftestD7.exe -f=tests\\projects\\.results\\delphi.7.xml\n        displayName: &#039;Execute tests&#039;\n\n- template: templates\/delphi-build.yml\n  parameters:\n    delphiVersion: xe4\n    project: tests\\projects\\selftest.XE4\n    searchPath: $(uses)\n    postBuild:\n      - script: tests\\projects\\.bin\\selftest.xe4.exe -f=tests\\projects\\.results\\delphi.xe4.xml\n        displayName: &#039;Execute tests&#039;\n        \n- task: PublishTestResults@2\n  inputs:\n    testResultsFormat: &#039;XUnit&#039;\n    testResultsFiles: &#039;*.xml&#039;\n    searchFolder: &#039;tests\/projects\/.results&#039;\n    testRunTitle: &#039;$(version) Test Results&#039;<\/code><\/pre>\n\n\n\n<p>This is a simplified version of the actual pipeline I&#8217;m using.  In the full pipeline I build for every version of Delphi on my agent.  I&#8217;ve reduced this to just <strong>Delphi 7<\/strong> and <strong>Delphi XE4<\/strong> for the purposes of this example (to show old and new-ish).<\/p>\n\n\n\n<p>We&#8217;ve also now progressed beyond the <strong>Hello Delphi<\/strong> project.  This pipeline builds and runs the self-tests for my <strong><a href=\"https:\/\/www.deltics.co.nz\/blog\/?s=smoketest\">Smoketest<\/a><\/strong><a href=\"https:\/\/www.deltics.co.nz\/blog\/?s=smoketest\"> unit testing framework<\/a> so some of the pre-amble in this script relates to that, as we&#8217;ll see.<\/p>\n\n\n\n<p>Let&#8217;s walk through it&#8230;<\/p>\n\n\n\n<p class=\"has-drop-cap\">The first <strong>powershell<\/strong> step runs <strong><a href=\"https:\/\/github.com\/GitTools\/GitVersion\">GitVersion<\/a><\/strong> which will determine an appropriate version with which to label any build based on the git commit history to the point at which this build runs.  The results of this are output in <strong>json<\/strong> format which I capture into a variable, <strong>$GitVersion<\/strong>.<\/p>\n\n\n\n<p>I then execute two <em>build commands<\/em>.<\/p>\n\n\n\n<p>These take the form of output echoed to the build agent which will be picked up by the build agent and interpreted as commands to be performed by the build agent on our behalf.  In a Powershell script we use <strong>Write-Host<\/strong> to output these commands.  In a <strong>cmd.exe<\/strong> script we would use <strong>ECHO<\/strong>.<\/p>\n\n\n\n<p class=\"has-background has-light-green-cyan-background-color\">It is also possible to <a href=\"https:\/\/github.com\/microsoft\/azure-pipelines-task-lib\">create entirely customised pipeline tasks using a provided Api<\/a>, with even greater access to controlling the build agent and interacting with the build pipeline.  But these console commands are a neat way to expose some of that functionality to any pipeline script, without requiring you to get your hands dirty with custom task development.<\/p>\n\n\n\n<p>The first build command sets a variable which can be picked up in later steps in this build.  This variable simply captures the <a href=\"https:\/\/semver.org\/\">SemVer<\/a> representation of the build version, as determined by GitVersion:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">Write-Host &quot;##vso[task.setvariable variable=version]$($GitVersion.SemVer)&quot;<\/code><\/pre>\n\n\n\n<p>Why not just keep using the <strong>$GitVersion<\/strong> variable itself ?<\/p>\n\n\n\n<p>Well, I can do that in <em>this<\/em> script, but later steps will run in separate processes and so won&#8217;t be aware of this variable.  By setting a variable on the agent itself, via the build agent command, the variable can be referenced in later steps and is actually substituted into those steps by the build agent, so this works if those steps are not script-based and cannot directly make use of environment or other script variables.<\/p>\n\n\n\n<p>The next build command is passed back by the build agent to the Azure DevOps build process itself and labels the currently running build with the specified version:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">Write-Host &quot;##vso[build.updatebuildnumber]$($GitVersion.SemVer)&quot;\n  displayName: Determine version for build<\/code><\/pre>\n\n\n\n<p>The initial <strong>##vso<\/strong> in the output is what marks these out as one of these special commands.  There are <em>many<\/em> <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/pipelines\/scripts\/logging-commands?view=azure-devops&amp;tabs=bash\">different commands available<\/a>.<\/p>\n\n\n\n<p>The next step is a simple <strong>cmd.exe<\/strong> script which uses an environment variable to build a search path that is needed by my selftest project.  Using the same <strong>##vso<\/strong> command, I stash this in a build variable which I call <strong>uses<\/strong>, though this time <strong>ECHO<\/strong> is used since this is a <strong>cmd.exe<\/strong> script:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">ECHO ##vso[task.setvariable variable=uses]%USES%<\/code><\/pre>\n\n\n\n<p class=\"has-drop-cap\">The next two steps build the self-test project for each of <strong>Delphi 7<\/strong> and <strong>XE4<\/strong>.  These steps use a build template (which we&#8217;ll look at next).  First let&#8217;s look at how I use the template to run tests.<\/p>\n\n\n\n<p>Build templates support parameters to configure them for a particular use, and here I&#8217;m using a <strong>postBuild<\/strong> parameter to specify an additional script to run after the build (only if it is successful).  Here&#8217;s that <strong>postBuild<\/strong> script as specified for the XE4 build:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">- template: templates\/delphi-build.yml\n  parameters:\n    delphiVersion: xe4\n    project: tests\\projects\\selftest.XE4\n    searchPath: $(uses)\n    postBuild:\n      - script: tests\\projects\\.bin\\selftest.xe4.exe -f=tests\\projects\\.results\\delphi.xe4.xml\n        displayName: &#039;Execute tests&#039;<\/code><\/pre>\n\n\n\n<p>I am relying here on the knowledge that my build template places any resulting <strong>.exe<\/strong> in a <strong>.bin<\/strong> folder which it will create under the project path.  It also creates a <strong>.results<\/strong> folder to hold any test results.<\/p>\n\n\n\n<p>Since the self-test project is itself a <strong><a href=\"https:\/\/www.deltics.co.nz\/blog\/?s=smoketest\">Smoketest<\/a><\/strong> suite, the results of the tests can be output in a file specified on the command line that runs the tests.  In this case I put the results from each run into that <strong>.results<\/strong> folder and since for this project I am running tests for each version of Delphi that I build with, I give those results a name that identifies the version of Delphi to which the test results relate.<\/p>\n\n\n\n<p>The final step runs a standard task from the Azure DevOps task library to scan for test results in a specified location (this can be very generic and find any test results produced during a run, but in this case I know where the results are so can direct the scan to that specific location).<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">- task: PublishTestResults@2\n  inputs:\n    testResultsFormat: &#039;XUnit&#039;\n    testResultsFiles: &#039;*.xml&#039;\n    searchFolder: &#039;tests\/projects\/.results&#039;\n    testRunTitle: &#039;$(version) Test Results&#039;<\/code><\/pre>\n\n\n\n<p>Any results found are sent back by this task to the Azure DevOps host where they will be ingested into the results for this build.  So now, as well as metrics for the build:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" loading=\"lazy\" width=\"640\" height=\"286\" src=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.45.07.jpg?resize=640%2C286&#038;ssl=1\" alt=\"\" class=\"wp-image-2898\" srcset=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.45.07.jpg?w=1024&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.45.07.jpg?resize=300%2C134&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.45.07.jpg?resize=768%2C343&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.45.07.jpg?resize=380%2C170&amp;ssl=1 380w\" sizes=\"(max-width: 640px) 100vw, 640px\" data-recalc-dims=\"1\" \/><\/figure>\n\n\n\n<p>I can now also view and dig into test results:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" loading=\"lazy\" width=\"640\" height=\"430\" src=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.59.28.jpg?resize=640%2C430&#038;ssl=1\" alt=\"\" class=\"wp-image-2899\" srcset=\"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.59.28.jpg?w=1024&amp;ssl=1 1024w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.59.28.jpg?resize=300%2C202&amp;ssl=1 300w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.59.28.jpg?resize=768%2C516&amp;ssl=1 768w, https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-11.59.28.jpg?resize=380%2C255&amp;ssl=1 380w\" sizes=\"(max-width: 640px) 100vw, 640px\" data-recalc-dims=\"1\" \/><\/figure>\n\n\n\n<p>This works because I have recently re-jiggered the output from <strong>Smoketest<\/strong> to output results in <strong>xUnit 2<\/strong> compatible format (though this is not yet in the publicly available version of <strong>Smoketest<\/strong>).<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p>Now let&#8217;s take a look at the build template itself:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">parameters:\n  delphiVersion: &#039;&#039;\n  project: &#039;&#039;\n  platform: &#039;&#039;\n  searchPath: &#039;&#039;\n  unitScopes: &#039;System;System.Win;Vcl;WinApi&#039;\n  fixPack: &#039;true&#039;\n  verbose: &#039;false&#039;\n  runIfSuccessful: &#039;true&#039;\n  postBuild: []\n\nsteps:\n- powershell: |\n    $delphi=&#039;${{ parameters.delphiVersion }}&#039;\n    $project=&#039;${{ parameters.project }}&#039;\n    $searchPath=&#039;${{ parameters.searchPath }}&#039;\n    $unitScopes=&#039;${{ parameters.unitScopes }}&#039;\n    $noFixPack=&#039;${{ parameters.noFixPack }}&#039;\n    $verbose=&#039;${{ parameters.verbose }}&#039;\n    $runIfSuccessful=&#039;${{ parameters.runIfSuccesful }}&#039;\n\n    $bits = if($platform -eq &#039;x64&#039;) {&#039;64&#039;} else {&#039;32&#039;}\n\n    $dpr=$(Split-Path -Leaf $project) + &#039;.dpr&#039;\n    $cfg=&#039;dcc&#039; + $bits + &#039;.cfg&#039;\n    $exe=&#039;.bin\\&#039; + $(Split-Path -Leaf $project) + &#039;.exe&#039;\n    $delphiRoot=&#039;c:\\dcc\\&#039; + $delphi\n    $delphiBin=$delphiRoot + &#039;\\bin&#039;\n    $delphiLib=$delphiRoot + &#039;\\lib&#039;\n        \n    # Assume that we will use the standard Delphi compiler but then\n    #  check for the presence of an appropriate IDE FixPack compiler\n    #  (unless the fixPack parameter is anything other than &#039;true&#039;).\n    #\n    # If an IDE FixPack compiler is found, use that instead.\n\n    $dcc = &#039;dcc&#039; + $bits\n    if($fixPack -eq &#039;true&#039;) {\n        Write-Host &#039;Checking for IDE FixPack compilers...&#039;\n\n        if(Test-Path -Path $($delphiBin + &#039;\\dcc&#039; + $bits + &#039;speed.exe&#039;)) {\n            $dcc = &#039;dcc32&#039;\n        } elseif(Test-Path -Path $($delphiBin + &#039;\\fastdcc&#039; + $bits + &#039;.exe&#039;)) {\n            $dcc = &#039;fastdcc&#039; + $bits\n        }\n    }\n    $dcc=$delphiBin + &#039;\\&#039; + $dcc + &#039;.exe&#039;\n    Write-Host $(&#039;Compiling with &#039; + $dcc)\n\n    # Modify the Delphi Lib path according to the specified Delphi version\n    #  and insert into the searchPath\n\n    if($delphi -eq &#039;xe&#039;) {\n        $delphiLib=$delphiLib + &#039;\\win32\\release&#039;\n    } elseif($delphi -gt &#039;xe&#039;) {\n        $delphiLib=$delphiLib + &#039;\\win&#039; + $bits + &#039;\\release&#039;\n    }\n    $searchPath=$delphiLib + &#039;;&#039; + $searchPath\n    Write-Host $(&#039;Delphi library path &#039; + $delphiLib)\n    Write-Host $(&#039;Using search path &#039; + $searchPath)\n        \n    # Construct compiler options for the dccNN.cfg file then create that file\n\n    Set-Location (Split-Path -Path $project)\n\n    if(-Not(Test-Path -Path .bin)) { New-Item .bin -ItemType directory | Out-Null }\n    if(-Not(Test-Path -Path .results)) { New-Item .results -ItemType directory | Out-Null }\n\n    $optD=&#039;-DCONSOLE&#039;\n    $optE=&#039;-E.bin&#039;\n    $optI=&#039;-I&#039; + $searchPath\n    $optN=&#039;-N.bin&#039;\n    $optR=&#039;-R&#039; + $searchPath\n    $optU=&#039;-U&#039; + $searchPath\n\n    if($delphi -gt &#039;xe&#039;) {\n        $optNS=&#039;-NS&#039; + $unitScopes\n    }\n        \n    Write-Host $(&#039;Creating compiler configuration &#039; + $cfg)\n\n    if(Test-Path -Path $cfg) { Remove-Item $cfg | Out-Null }\n    New-Item $cfg -ItemType file | Out-Null\n\n    Add-Content $cfg $optD\n    Add-Content $cfg $optE\n    Add-Content $cfg $optI\n    Add-Content $cfg $optN\n    Add-Content $cfg $optR\n    Add-Content $cfg $optU\n    if($delphi -gt &#039;xe&#039;) {\n        Add-Content $cfg $optNS\n    }\n\n    # Remove any previous build and invoke the command-line compiler\n        \n    if(Test-Path -Path $exe) { Remove-Item $exe | Out-Null }\n\n    $cmd=$dcc + &#039; &#039; + $dpr + &#039; -CC&#039;\n    Write-Host $(&#039;Compiling with &#039; + $cmd)\n    if($verbose -eq &#039;true&#039;) {\n        Invoke-Expression $cmd\n    } else {\n        Invoke-Expression $cmd | Select-String -Pattern &#039;Fatal: &#039;,&#039;Hint: &#039;,&#039;Warning: &#039;\n    }\n\n    if(Test-Path -Path $exe) {\n        Write-Host $(&#039;Build succeeded! :)  [&#039; + $exe + &#039;]&#039;)\n    } else {\n        Write-Host &quot;##vso[task.logissue type=error]Build failed.  :(&quot;\n        exit 1\n    }\n  displayName: &#039;Delphi ${{ parameters.delphiVersion }} Build of ${{ parameters.project }} &#039;\n- ${{ parameters.postBuild }}<\/code><\/pre>\n\n\n\n<p>Most of this is the <strong>powershell <\/strong>build script that does the actual build, which is much more sophisticated than <a href=\"https:\/\/www.deltics.co.nz\/blog\/posts\/2861\">the simple test we saw last time<\/a>.<\/p>\n\n\n\n<p class=\"has-drop-cap\">Starting at the beginning, the initial <strong>parameters<\/strong> section identifies the parameters which may be passed in to this template, as we saw in the build itself.  The most interesting of these is the <strong>postBuild<\/strong> parameter which as we saw accepted an entire script to be run by this template.  But we can see here that this is not difficult to achieve &#8211; far from it in fact.<\/p>\n\n\n\n<p>The first thing that the build script does is place most of these parameters into script variables.  This isn&#8217;t strictly necessary, but has some benefits.<\/p>\n\n\n\n<ol><li>It does allow us to manipulate the values if we wish.  Parameter values are directly substituted into the script before it runs &#8211; i.e. you cannot treat parameters themselves as variables.<\/li><li>(and mainly) I&#8217;m doing this just to make it slightly less cumbersome to reference them later in the script. \ud83d\ude42<\/li><\/ol>\n\n\n\n<p>What then follows is some further setup of script variables, driven by the version of Delphi identified for the build.  The aim here is to setup variables that identify the compiler executable (in the relevant <strong>bin<\/strong> folder) and the path to the VCL and RTL units that will be needed for the build (in the corresponding <strong>lib<\/strong> folder).<\/p>\n\n\n\n<p>As I mentioned, my build agent has the <strong>bin<\/strong> and <strong>lib<\/strong> folders for many different versions of Delphi on it.  By following some conventions I&#8217;m able to construct paths to those folders by dropping in the Delphi version as required, also taking account of the target platform in later Delphi versions (to distinguish between <strong>x86<\/strong> and <strong>x64<\/strong> builds).<\/p>\n\n\n\n<p>It should be fairly obvious how this is achieved by looking at a fragment of the file system as it is found on the build agent:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">c:\\dcc\\7\n         \\bin\n            \\dcc32.exe\n         \\lib\n c:\\dcc\\xe4\n          \\bin\n             \\dcc32.exe\n             \\dcc64.exe\n          \\lib\n             \\win32\n                  \\debug\n                  \\release\n             \\win64\n                  \\debug\n                  \\release<\/pre>\n\n\n\n<p>The next step is a little bit of finesse.<\/p>\n\n\n\n<p>Alongside the stock compilers, in each bin folder I have also put copies of <a href=\"https:\/\/www.idefixpack.de\/\">the &#8220;speed&#8221; and &#8220;fast&#8221; compilers from the amazing Andreas Hausladen<\/a>.  The build template checks for these, and if found will invoke those in preference to the stock compiler, to eek a little extra efficiency for my builds.<\/p>\n\n\n\n<p>There is then a final tweak to the <strong>libPath<\/strong> for later Delphi versions before the <strong>.bin<\/strong> and <strong>.results<\/strong> folders are created in the project location.<\/p>\n\n\n\n<p>The script then constructs a compiler configuration file.  I anticipate this is a section that will either be replaced in the future with something driven by project configuration or will grow to a point where reliance on those project files is avoided entirely.<\/p>\n\n\n\n<p>There are arguments both ways as to which is the more desirable.<\/p>\n\n\n\n<ul><li>On the one hand, having builds use project configurations ensures that CI\/CD builds are consistent with the builds produced by developers on their machines.<\/li><li>On the other hand, using project configurations is more difficult for older Delphi versions that don&#8217;t directly support configuration sets (like it or not, some people are still using those, and I still include tests for those in my framework libraries).  It also means that a CI\/CD build could <em>inadvertently<\/em> be switched to alternate settings by a developer pushing changes to their project file after making some change to compiler settings that they did not intend.<\/li><\/ul>\n\n\n\n<p>Either way, for my purposes right now, building and using a configuration file is the way I&#8217;ve decided to go for now.<\/p>\n\n\n\n<p class=\"has-drop-cap\">Once the configuration file is prepared, the compiler is then invoked.  If the <strong>verbose<\/strong> template parameter has not been set <strong>true<\/strong> the compiler output is filtered to only show <strong>Hints<\/strong>, <strong>Warnings<\/strong> and <strong>Errors<\/strong>.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\"> if($verbose -eq &#039;true&#039;) {\n        Invoke-Expression $cmd\n    } else {\n        Invoke-Expression $cmd | Select-String -Pattern &#039;Fatal: &#039;,&#039;Hint: &#039;,&#039;Warning: &#039;\n    }<\/code><\/pre>\n\n\n\n<p>The Delphi compiler is inordinately &#8220;chatty&#8221; otherwise, spewing out a host of largely useless information.  This keeps the information in the job output to a more digestible level.<\/p>\n\n\n\n<p>The only thing left to do is check that the build was successful (an EXE was created) and just to make sure that the job status is accurately reflected (i.e. regardless of any exit code set &#8211; or not &#8211; by the compiler), if we don&#8217;t find an EXE then we use another of the built-in build commands to report to the build agent that the job has failed.  Otherwise we report our success to the console.<\/p>\n\n\n\n<p>The very last step then simply embeds whatever was passed in the <strong>postBuild<\/strong> parameter, having the effect of invoking that post-build script.  If no <strong>postBuild<\/strong> step is passed, this is an empty step and so simply does nothing.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">    if(Test-Path -Path $exe) {\n        Write-Host $(&#039;Build succeeded! :)  [&#039; + $exe + &#039;]&#039;)\n    } else {\n        Write-Host &quot;##vso[task.logissue type=error]Build failed.  :(&quot;\n        exit 1\n    }\n  displayName: &#039;Delphi ${{ parameters.delphiVersion }} Build of ${{ parameters.project }} &#039;\n- ${{ parameters.postBuild }}<\/code><\/pre>\n\n\n\n<p>Notice that there is no explicit check for a successful build that makes the postBuild script conditional.  This is taken care of the fact that if the build itself fails then the default behaviour of the build agent is to skip any subsequent steps.<\/p>\n\n\n\n<p>This also means that if we do want any <strong>postBuild<\/strong> step to be run even if the build fails, then we can do that by simply <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/pipelines\/process\/conditions?view=azure-devops&amp;tabs=yaml\">including the appropriate <\/a><strong><a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/pipelines\/process\/conditions?view=azure-devops&amp;tabs=yaml\">condition<\/a><\/strong><a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/pipelines\/process\/conditions?view=azure-devops&amp;tabs=yaml\"> property<\/a> on the <strong>postBuild<\/strong> script we pass in.  e.g:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">- template: templates\/delphi-build.yml\n  parameters:\n    delphiVersion: xe4\n    project: tests\\projects\\selftest.XE4\n    searchPath: $(uses)\n    postBuild:\n      - script: tests\\projects\\.bin\\selftest.xe4.exe -f=tests\\projects\\.results\\delphi.xe4.xml\n        condition: always()\n        displayName: &#039;Execute tests&#039;<\/code><\/pre>\n\n\n\n<p>This obviously doesn&#8217;t make sense in this case and is just an example.<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p class=\"has-drop-cap\">Whew!  That&#8217;s been something of a whilst-stop tour of Azure DevOps and a glimpse inside the CI\/CD infrastructure I&#8217;m putting in place around my projects.<\/p>\n\n\n\n<p>The best part of this is that everything I&#8217;ve shown so far is available for FREE, as in beer, as long as you put in the hours to configure it all.<\/p>\n\n\n\n<p>The only thing that cost any $&#8217;s for me in this exercise (aside from Delphi, which I already had) was the Intel NUC PC that I&#8217;m using as the build machine in this case, but even that was relatively small beer, just barely over NZ$500 (~US$320) including RAM and SSD etc.<\/p>\n\n\n\n<p>What I&#8217;ve shown is not actually complete though.  There are some key steps that I&#8217;ve deliberately left out as I&#8217;m not yet quite ready to share.  But I&#8217;m getting close.<\/p>\n\n\n\n<p>As a little tease, I&#8217;ll just ask&#8230; have you heard of <strong>nuget<\/strong> ?  \ud83d\ude09<\/p>\n","protected":false},"excerpt":{"rendered":"<p><span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">[Estimated Reading Time: <\/span> <span class=\"rt-time\">9<\/span> <span class=\"rt-label rt-postfix\">minutes]<\/span><\/span> A more complete build script, re-usable in the form of a template, that caters for different Delphi versions, combined with a demonstration of running unit tests and capturing results for reporting and analysis in Azure DevOps Pipelines.<\/p>\n","protected":false},"author":2,"featured_media":2897,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"jetpack_publicize_message":"Azure DevOps and Delphi - Template for Builds + Running Tests and Capturing Results","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":[]},"categories":[324,322,323,4,321],"tags":[326,331,251,252,327,292,329],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Screen-Shot-2019-09-12-at-14.25.20.jpg?fit=386%2C386&ssl=1","jetpack_shortlink":"https:\/\/wp.me\/p1TKYv-Kq","jetpack_sharing_enabled":true,"jetpack-related-posts":[{"id":2861,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2861\/","url_meta":{"origin":2878,"position":0},"title":"Azure DevOps &#8211; Building Some Code","date":"09 Sep 2019","format":false,"excerpt":"In this post we create a (very!) simple project, build it using Delphi (7) and run it. All with Azure DevOps.","rel":"","context":"In &quot;automation&quot;","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/pipelines-hero-code-1024x256.jpg?fit=1024%2C256&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":2919,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2919\/","url_meta":{"origin":2878,"position":1},"title":"Azure DevOps &#8211; Now You Too Can Use My Template(s)","date":"14 Sep 2019","format":false,"excerpt":"Learn how you too can use my Delphi build template in your own Azure DevOps pipelines, and a sneak preview of something special coming soon...","rel":"","context":"In &quot;automation&quot;","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/github-512.png?fit=512%2C512&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":2936,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2936\/","url_meta":{"origin":2878,"position":2},"title":"Azure DevOps &#8211; Iterative Insertion Fixed!","date":"19 Sep 2019","format":false,"excerpt":"I figured out the iterative insertion problem and my build pipeline is now TIGHT! Fixing it was super-easy in fact, barely an inconvenience.","rel":"","context":"In &quot;automation&quot;","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/pipelines-hero-code-1024x256.jpg?fit=1024%2C256&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":2659,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2659\/","url_meta":{"origin":2878,"position":3},"title":"Azure DevOps and Delphi &#8211; Build Agents","date":"06 Sep 2019","format":false,"excerpt":"The first in a series of posts exploring build and test automation for Delphi projects using Azure DevOps.","rel":"","context":"In &quot;automation&quot;","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/pipelines-hero-1-1024x256.jpg?fit=1024%2C256&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":2931,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2931\/","url_meta":{"origin":2878,"position":4},"title":"&#8216;Sorry, this script is too long&#8230;&#8217;","date":"18 Sep 2019","format":false,"excerpt":"An object lesson on the importance of defensive programming and providing helpful error messages to your users.","rel":"","context":"In &quot;Azure DevOps&quot;","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Mark_Twain_life_1900s.jpg?fit=883%2C331&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":2968,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2968\/","url_meta":{"origin":2878,"position":5},"title":"Azure DevOps + GitHub = GitHub++","date":"23 Sep 2019","format":false,"excerpt":"A look at some of the integrations that \"just work\" when you combine Azure DevOps and GitHub","rel":"","context":"In &quot;automation&quot;","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/azure-devopsgithub.png?fit=1024%2C341&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]}],"_links":{"self":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/2878"}],"collection":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/comments?post=2878"}],"version-history":[{"count":14,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/2878\/revisions"}],"predecessor-version":[{"id":2913,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/2878\/revisions\/2913"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/media\/2897"}],"wp:attachment":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/media?parent=2878"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/categories?post=2878"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/tags?post=2878"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}