{"id":2931,"date":"2019-09-18T18:12:39","date_gmt":"2019-09-18T06:12:39","guid":{"rendered":"https:\/\/www.deltics.co.nz\/blog\/?p=2931"},"modified":"2019-09-18T20:56:05","modified_gmt":"2019-09-18T08:56:05","slug":"sorry-this-script-is-too-long","status":"publish","type":"post","link":"https:\/\/www.deltics.co.nz\/blog\/posts\/2931\/","title":{"rendered":"&#8216;Sorry, this script is too long&#8230;&#8217;"},"content":{"rendered":"<span class=\"rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">[Estimated Reading Time: <\/span> <span class=\"rt-time\">6<\/span> <span class=\"rt-label rt-postfix\">minutes]<\/span><\/span>\n<p>If Mark Twain was hired to write error messages for the Azure DevOps team, he might have deployed his own, suitably paraphrased quote, completing it with&#8230; &#8216;<em>Perhaps you should take some time to make it shorter ?<\/em>&#8216;<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>I&#8217;ve been wrestling with a problem today with my Delphi build template that is a classic case of misdirection and inadequate error handling (or rather, unhelpful, unadorned error messages being passed to the end-user without further explanation or suggestion).<\/p>\n\n\n\n<p>A bit of background&#8230;<\/p>\n\n\n\n<p class=\"has-drop-cap\">As noted before, much of the <strong>Delphi<\/strong> work I have done (and am returning to) involves supporting many different versions of Delphi, from Delphi 7 to current.  As a result, my CI\/CD pipelines need to build code with all of those Delphi versions and (increasingly) run tests to exercise it.<\/p>\n\n\n\n<p>So I was making changes to my build template to enable me to pass in an array of <strong>Delphi<\/strong> versions and have it perform builds for all those versions.<\/p>\n\n\n\n<p>This comes with some downsides, which we&#8217;ll perhaps come back to.  But my initial problem was how to achieve it.<\/p>\n\n\n\n<p>I first decided to try <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/pipelines\/process\/templates?view=azure-devops#iterative-insertion\"><strong>Iterative Insertion<\/strong><\/a>, as this seemed to be suited to the job.  That is, I wanted to create a further template which would iteratively insert into itself the steps needed to in-turn invoke my build template for each <strong>Delphi<\/strong> version that I specified.<\/p>\n\n\n\n<p>My goal was to be able replace a list of over a dozen individual build steps with just one.  To give you some idea, consider a build involving just 3 Delphi versions (let&#8217;s say 7, 2007 and 10.3 Rio for example):<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">- template: delphi-build.yml@templates\n  parameters:\n    delphiVersion: 7\n    project: tests\\tests\n    preBuildInline: duget restore --noUnpack\n    postBuildInline: .bin\\tests -f=.results\\delphi.7.xml\n- template: delphi-build.yml@templates\n  parameters:\n    delphiVersion: 2007\n    project: tests\\tests\n    preBuildInline: duget restore --noUnpack\n    postBuildInline: .bin\\tests -f=.results\\delphi.2007.xml\n- template: delphi-build.yml@templates\n  parameters:\n    delphiVersion: 10.3\n    project: tests\\tests\n    preBuildInline: duget restore --noUnpack\n    postBuildInline: .bin\\tests -f=.results\\delphi.10.3.xml\n- etc<\/code><\/pre>\n\n\n\n<p>The goal was to be able to achieve the same thing with just this one step:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">- template: delphi-builds.yml@templates\n  parameters:\n    delphiVersion: 7 2007 10.3\n    project: tests\\tests\n    preBuildInline: duget restore --noUnpack\n    postBuildInline: .bin\\tests -f=.results\\delphi.$($delphiVersion).xml<\/code><\/pre>\n\n\n\n<p>Neat, huh ?<\/p>\n\n\n\n<p>Long story short, I could not figure out how to do this with <strong>Iterative Insertion<\/strong>. The &#8216;error messages&#8217; from the YAML processor were less than helpful and I couldn&#8217;t find anyone doing anything similar using this approach.<\/p>\n\n\n\n<p>So I tried coming at it from a different angle.<\/p>\n\n\n\n<p class=\"has-drop-cap\">Instead of trying to dynamically generate a template to call another template, I used the dynamic nature of <strong>Powershell<\/strong> itself.  After an initial struggle, trying to figure out how to pass an array of <strong>Delphi<\/strong> versions as a parameter to a template and then have that picked up as an array of strings by <strong>Powershell<\/strong>, I realised that it was far simpler than that.<\/p>\n\n\n\n<p>The simple string syntax already gave me everything I wanted and needed.  Since YAML does not require string values to be quoted, simply passing a space-separated list of values did the job and was actually cleaner anyway.<\/p>\n\n\n\n<p>This meant that my target syntax could actually be supported directly by my existing template:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-yaml\">- template: delphi-build.yml@templates\n  parameters:\n    delphiVersion: 7 2007 10.3\n    project: tests\\tests\n    preBuildInline: duget restore --noUnpack\n    postBuildInline: .bin\\tests -f=.results\\delphi.$($delphiVersion).xml<\/code><\/pre>\n\n\n\n<p>All I needed to do was refactor the <strong>Powershell<\/strong> script in that template to create a <em>function<\/em> to perform the build with the script itself becoming a processor which turned the <strong><code>delphiVersion<\/code><\/strong> parameter into an array and then called that build function iteratively for each version in that array (i.e. each version space separated in the parameter).<\/p>\n\n\n\n<p>Neat.<\/p>\n\n\n\n<p class=\"has-drop-cap\">A few short moments later I had an almost working script.  The only problem I had was that I forgot to deal with one important side effect of now calling this script effectively multiple times.<\/p>\n\n\n\n<p>The script set the working location for the build to the folder containing the project being compiled.  When building for multiple Delphi versions this worked fine for the very first version compiled and then failed miserably for all subsequent compilations because the subsequent invocations of the build function were <em>starting<\/em> in that location rather than the <em>root<\/em> of the repo.<\/p>\n\n\n\n<p>No problem, one of the great things about Powershell is that it features many of the capabilities that we&#8217;re used to in other programming languages.  In this case the familiar <code>try<\/code>\/<code>finally<\/code> was the tool I reached for.<\/p>\n\n\n\n<p>Before calling <code>Set-Location<\/code> to change the current directory, simply stash the result of <code>Get-Location<\/code>, wrap the important parts of the build process in a <code>try<\/code> \/ <code>finally<\/code> and call <code>Set-Location<\/code> again in the <code>finally<\/code> to restore the previous location.<\/p>\n\n\n\n<p>What could be simpler ?<\/p>\n\n\n\n<p>This simple change completely broke my script.  And I mean:  <em>Completely<\/em>.<\/p>\n\n\n\n<p>The pipeline started complaining that my <code>try&lt;code&gt; block was missing a <\/code>catch<\/code> or <code>finally<\/code> (it obviously wasn&#8217;t).  I tidied up some of my formatting inconsistencies, just on the off-chance that there was something in there throwing things off with my bracketing (though there were no complaints on that score).<\/p>\n\n\n\n<p>Ooops.  Spoke too soon.  Although I had not introduced or removed any bracketing in my tidy ups, suddenly my pipeline started complaining that my <code>{}<\/code>&#8216;s were mis-matched.<\/p>\n\n\n\n<p>After painstakingly re-assuring myself that this wasn&#8217;t the case (it wasn&#8217;t) I turned my suspicious gaze on YAML whitespace indentation &#8211; this had caught me out before.<\/p>\n\n\n\n<p>All in vain.<\/p>\n\n\n\n<p>But I had undertaken more tidying up along the way and now <strong>Azure DevOps<\/strong> itself was complaining.  Up to now the errors had occurred when the build job reached my step on the build machine and executed the Powershell.  Now Azure DevOps was point-blank refusing to even start a run.  Now there was some issue right at the top, line 17 to be precise, the very beginning of the script step.  And now the problem was something about not being able to &#8216;<em>convert from an array to a string<\/em>&#8216;.<\/p>\n\n\n\n<p>I was clearly getting nowhere trying to fix the script as it stood.  In fact, going backwards!<\/p>\n\n\n\n<p class=\"has-drop-cap\">Not being able to make sense of the errors, I decided to re-construct the entire script piece by piece.  Stripping it back to nothing then adding chunks of functionality a few lines at a time until it broke.  The idea was that this would help me identify <em>where<\/em> exactly in the script the problem was and so hopefully make it easier to spot.<\/p>\n\n\n\n<p>This started well, with my script instantly back to glowing health, even if not doing very much.  In fact, I got all the way to the final dozen or so lines of code before it broke.  These final few lines were simple stuff so figuring out where the problem was should be simple.<\/p>\n\n\n\n<p>The bad news: Yes, these lines were indeed <em>very<\/em> simple.  There was clearly and self-evidently <em>nothing wrong with them at all<\/em>.<\/p>\n\n\n\n<p>Yet, without those 12 lines my script compiled and ran just fine.  With them, it would not even run.<\/p>\n\n\n\n<p>On a nervous hunch, I added them back in, but commented them out.<\/p>\n\n\n\n<p>And still the pipeline <em>would not run<\/em>.<\/p>\n\n\n\n<p><strong>Conclusion: <\/strong><em>The problem was not caused by anything the code was doing &#8211; commented out code doesn&#8217;t do anything.  The problem was simply the code being there at all!<\/em><\/p>\n\n\n\n<p>There was one thing that might explain both the errors I was seeing before and this new discovery: <em>An upper limit on the length of a script that can be included in an Azure Pipeline step!<\/em><\/p>\n\n\n\n<p>And sure enough, there appears to be such a limit.<\/p>\n\n\n\n<p class=\"has-background has-pale-pink-background-color\">The different error messages were presumably the result of this limit being run into at different stages in the process.<\/p>\n\n\n\n<p>If the &#8220;raw&#8221; script itself was too big, then the problem of not being able to <em>convert an array to a string<\/em> presumably comes from the pipeline trying to parse the template itself.<\/p>\n\n\n\n<p>But if the raw script was just within the limit, the problem might then have been caused when substitutions and insertions were applied (e.g. the <code>preBuildInline<\/code> and <code>postBuildInline<\/code> script fragments that my consuming pipeline inserted).  This may have pushed the script over the bounds of whatever buffer was being used to prep it.  This truncated buffer was then being passed on to the build agent where <strong>Powershell<\/strong> itself would then choke on the syntax errors caused by that truncation.  Those errors would vary according to where the truncation had occurred, and none of them would make sense when considered in the context of the whole script.<\/p>\n\n\n\n<p>It all started to make sense, but why oh why could there not have been a straightforward error message to tell me that this length limit had been exceeded!? It would have saved me hours!<\/p>\n\n\n\n<p>From subsequent digging through forums and the like, it seems that the limit is 5,000 characters.  This seems more or less consistent with my own observations.  It\u2019s difficult to be very precise as the use of substitutions and insertions in a template seem to be a contributing factor.  i.e. it\u2019s not only a question of how many characters there are in the script source in a template or step, but the total characters after any substitutions have taken place.<\/p>\n\n\n\n<p>Be that as it may, all I have to do now is figure out how to scrunch this script down without making it impenetrable and retaining the functionality I want and need.<\/p>\n\n\n\n<p>Let this be a lesson in the utility of helpful error messages (and the unnecessary pain that can be caused by their omission).<\/p>\n\n\n\n<p class=\"has-background has-drop-cap has-pale-cyan-blue-background-color\"><strong>Footnote: <\/strong>I mentioned that building with multiple versions of <strong>Delphi<\/strong> in a single step has a downside.  Well, it is this: Since all of the compilations with the various Delphi versions specified are performed in a single step, the resulting output from the build pipeline can only tell me that <strong>Delphi<\/strong> compilation failed.  I then need to scour through the logs of that one step to figure out <em>which<\/em> Delphi versions failed. <strong> Iterative Insertion<\/strong> would avoid that by creating separate steps for each Delphi version, so it was a real shame that I couldn&#8217;t get that to work.  So for now, I&#8217;m going to keep the multiple Delphi version support for some perhaps limited occasions when it may be useful, but use a different trick to keep my main pipelines trimmed to a minimum.<\/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\">6<\/span> <span class=\"rt-label rt-postfix\">minutes]<\/span><\/span> An object lesson on the importance of defensive programming and providing helpful error messages to your users.<\/p>\n","protected":false},"author":2,"featured_media":2932,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":[]},"categories":[322,323,4,321,1],"tags":[326,292,328,338],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/www.deltics.co.nz\/blog\/wp-content\/uploads\/Mark_Twain_life_1900s.jpg?fit=883%2C331&ssl=1","jetpack_shortlink":"https:\/\/wp.me\/p1TKYv-Lh","jetpack_sharing_enabled":true,"jetpack-related-posts":[{"id":2659,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2659\/","url_meta":{"origin":2931,"position":0},"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":2861,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2861\/","url_meta":{"origin":2931,"position":1},"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":2931,"position":2},"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":2931,"position":3},"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":2878,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2878\/","url_meta":{"origin":2931,"position":4},"title":"Azure DevOps &#8211; Template for Builds + Running Tests and Capturing Results","date":"12 Sep 2019","format":false,"excerpt":"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.","rel":"","context":"In &quot;automation&quot;","img":{"alt_text":"","src":"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&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":2968,"url":"https:\/\/www.deltics.co.nz\/blog\/posts\/2968\/","url_meta":{"origin":2931,"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\/2931"}],"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=2931"}],"version-history":[{"count":3,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/2931\/revisions"}],"predecessor-version":[{"id":2935,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/posts\/2931\/revisions\/2935"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/media\/2932"}],"wp:attachment":[{"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/media?parent=2931"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/categories?post=2931"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.deltics.co.nz\/blog\/wp-json\/wp\/v2\/tags?post=2931"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}