<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Tech Tips on But it works on my PC!</title>
    <link>https://blog.richardfennell.net/tags/tech-tips/</link>
    <description>Recent content in Tech Tips on But it works on my PC!</description>
    <image>
      <title>But it works on my PC!</title>
      <url>https://blog.richardfennell.net/images/papermod-cover.png</url>
      <link>https://blog.richardfennell.net/images/papermod-cover.png</link>
    </image>
    <generator>Hugo -- 0.147.0</generator>
    <language>en</language>
    <lastBuildDate>Fri, 31 Oct 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog.richardfennell.net/tags/tech-tips/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Interesting side effect with the Azure DevOps cache if misconfigured</title>
      <link>https://blog.richardfennell.net/posts/interesting-side-effect-with-azdo-cache/</link>
      <pubDate>Fri, 31 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/interesting-side-effect-with-azdo-cache/</guid>
      <description>&lt;p&gt;I recently came across an interesting side effect with the Azure DevOps cache task if its settings are not correctly configured. One that caused me to get somewhat confused before I realised what had occurred.&lt;/p&gt;
&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;I had a working pipeline that as part of its build process ran the &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=dependency-check.dependencycheck&#34;&gt;OWASP Dependency Checker task&lt;/a&gt;. This can be slow to run as it has to download the current vulnerability database. To try to speed my builds I have been using the &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/cache-v2?view=azure-pipelines&#34;&gt;cache task&lt;/a&gt; to cache the current pipeline run&amp;rsquo;s downloaded vulnerability database, so on the next run the vast majority of the database is already downloaded.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I recently came across an interesting side effect with the Azure DevOps cache task if its settings are not correctly configured. One that caused me to get somewhat confused before I realised what had occurred.</p>
<h1 id="the-problem">The Problem</h1>
<p>I had a working pipeline that as part of its build process ran the <a href="https://marketplace.visualstudio.com/items?itemName=dependency-check.dependencycheck">OWASP Dependency Checker task</a>. This can be slow to run as it has to download the current vulnerability database. To try to speed my builds I have been using the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/cache-v2?view=azure-pipelines">cache task</a> to cache the current pipeline run&rsquo;s downloaded vulnerability database, so on the next run the vast majority of the database is already downloaded.</p>
<p>The pipeline YAML is as follows</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">PowerShell@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Find the NVD DB path to cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetType</span><span class="p">:</span><span class="w"> </span><span class="l">inline</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        $nvdcachepath = $(get-childitem &#34;$(Agent.WorkFolder)\_tasks\dependency-check-build-task*\*.*.*\dependency-check\data&#34;).FullName
</span></span></span><span class="line"><span class="cl"><span class="sd">        echo &#34;##vso[task.setvariable variable=nvdcachepath;]$nvdcachepath&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">Cache@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Cache NVD data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;&#34;NVDCache&#34; | &#34;$(Agent.OS)&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">$(nvdcachepath)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restoreKeys</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        NVDCache | &#34;$(Agent.OS)&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        NVDCache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># other build tasks</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">JavaToolInstaller@0 </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Install Java needed for the Dependency Check</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">versionSpec</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;11&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jdkArchitectureOption</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;x64&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jdkSourceOption</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;PreInstalled&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">dependency-check.dependencycheck.dependency-check-build-task.dependency-check-build-task@6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Dependency Check</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">projectName</span><span class="p">:</span><span class="w"> </span><span class="l">Identity Server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">scanPath</span><span class="p">:</span><span class="w"> </span><span class="l">CCC.Web.IdentityAndSSO.IdentityServer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">HTML,XML</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">additionalArguments</span><span class="p">:</span><span class="w"> </span>--<span class="l">nvdApiKey $(nvdapikey)</span><span class="w">
</span></span></span></code></pre></div><p>This has all been working well, but the YAML has a potential flaw, can you spot it?</p>
<p>The issue appeared when, due to the server that provides the OWASP vulnerability database being unavailable the dependency check task was temporarily disabled.</p>
<p>This meant that the dependency check task was not downloaded, so the Powershell script to find the vulnerability database folder returned an empty string as it could find no matching folder.</p>
<p>This is important as it is this string that controls what is cached. With no folder passed into the cache task, the whole of the pipeline working folder is cached. In effect the restoring of the cache is checking out the source as it was on the last successful build on the branch. So you are not building the commit you think you are, but an older commit, the source code that was cached.</p>
<h1 id="the-solution">The Solution</h1>
<p>There are a number of options to avoid this problem</p>
<ol>
<li>Don&rsquo;t use the Cache task</li>
<li>Don&rsquo;t disable any task that produces folders your Cache task is meant caches</li>
<li>If you disable a task who&rsquo;s data you cache also disable the Cache task</li>
<li>Put some logic around the cache task e.g
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">Cache@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">and(succeeded(), ne(variables[&#39;nvdcachepath&#39;], &#39;&#39;))</span><span class="w">
</span></span></span></code></pre></div></li>
</ol>
<p>All are valid for different scenarios, the choice is down to your use case.</p>
<p>The overall point is make sure you are caching what you think you are caching, if not constrained the Cache task will cache everything it can.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting a Surface Hub v1 Working with a Replacement PC</title>
      <link>https://blog.richardfennell.net/posts/getting_a_surfacehub_working_with_a_replacement_pc/</link>
      <pubDate>Tue, 28 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting_a_surfacehub_working_with_a_replacement_pc/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Updated 30-Oct-2025&lt;/strong&gt; Added more details on Screen Refresh Rate and Teams Room App&lt;/p&gt;&lt;/blockquote&gt;
&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;We have owned a Surface Hub v1 for a number of years, and it has served us well. However, with Microsoft ending support for Windows 10 it was in danger of becoming a large piece of sculpture in the office. This is not just because we did not want to run a Windows 10 device when security patches were not available, but that the embedded version of Teams would not even load.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>Updated 30-Oct-2025</strong> Added more details on Screen Refresh Rate and Teams Room App</p></blockquote>
<h1 id="the-problem">The Problem</h1>
<p>We have owned a Surface Hub v1 for a number of years, and it has served us well. However, with Microsoft ending support for Windows 10 it was in danger of becoming a large piece of sculpture in the office. This is not just because we did not want to run a Windows 10 device when security patches were not available, but that the embedded version of Teams would not even load.</p>
<p>In theory the answer was easy, <a href="https://learn.microsoft.com/en-us/surface-hub/surface-hub-v1-plan-eos">flip a switch on the Surface Hub so it uses an external replacement PC</a> as opposed to it&rsquo;s built in one. However, this process turned out to be harder than we expected as the Surface Hub is very picky over hardware settings.</p>
<h1 id="the-solution">The Solution</h1>
<p>The whole process is very well documented by <a href="https://rwold.net/how-to-use-surface-hub-v1-as-external-interactive-display/">Ryan Wold</a>, so I won&rsquo;t repeat the details. However, I will mention the kit and settings we ended up using as we found getting any output on the Surface Hub meant using exactly the correct settings.</p>
<p>We aimed to use spare kit we had around, so our replacement PC was an old <a href="https://www.lenovo.com/us/en/p/desktops/thinkcentre/m-series-sff/thinkcentre-m910s">Lenovo M910s desktop PC</a>. We picked this particular unit as we had at some point in the past fitted it with an <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/geforce-gtx-750-ti/">Nvidia Geforce GTX 750Ti </a> as we knew that a dedicated GPU would be needed to drive the 4K screen.</p>
<p>The consistent issue we had trying to get this working was that the PC  would boot, we would see the BIOS boot screen, Windows would start to load, but as soon as the loading Windows spinner appeared the Surface Hub screen would go blank. If we had a second display attached, we could see that Windows could not detect the Surface Hub as a display.</p>
<p>The critical item it turned out was the display one, this needed to be an active cable. We ended up with a <a href="https://www.amazon.co.uk/dp/B0BYJP8QQG?ref=ppx_yo2ov_dt_b_fed_asin_title">BENFEI HDMI to DisplayPort Adapter</a>. Once we used this some of our display issues were sorted, Windows could detect the Surface Hub consistently.</p>
<p>The other critical step was that we had to set the resolution to 4K (3840 x 2160) and specifically with the refresh rate set at 30Hz. If we had a more powerful GPU we think we could have run at the default rate of 120Hz, but that was not an option with the kit we had.</p>
<p>30Hz is not the default refresh rate when the Surface Hub is detected as a monitor, so we got the blank screen. To change the refresh rate we had to have a second display attached. This is not a problem you might think, make the edit and remove the second display. However, Windows stores the refresh rates separately for each screen setup e.g. single display, dual display etc. This meant when we disconnected the second display the refresh rate for the Surface Hub returned to its 120Hz default.</p>
<p>So for now we need to keep the second display attached</p>
<p>But not all the setup was so hard, getting the Surface Hub cameras, audio devices, touch screen and pens working was easy. All that needed was a USB cable and the <a href="https://www.microsoft.com/en-us/download/details.aspx?id=52210&amp;msockid=34c33284916a6b933d7324a3908a6aed">right drivers</a></p>
<h1 id="sorting-the-refresh-rate">Sorting the Refresh Rate</h1>
<p>The solution to the refresh rate issue was to use the <a href="https://customresolutionutility.net/">CRU Display Utility</a>.</p>
<p>With the PC running with with dual monitors:</p>
<ol>
<li>Download, unzip and run the the <strong>CRU Utility</strong></li>
<li>Selected the <strong>Surface Hub (active)</strong> display and altered the detailed resolutions order. You need the 30Hz option to be the first entry in the list</li>
<li>Repeat Step 2 for any other <strong>Surface Hub</strong> entries in the list (not sure if this step is essential, but quick to do)</li>
<li>Pressed Save and exited CRU</li>
<li><strong>This is the important step we initially missed</strong> Use the CRU provided <strong>restart.exe</strong> tool to restart the graphic subsystem and hence read the changed order.</li>
<li>I could then disconnect the 2nd monitor, the display a rescan was done, but unlike in the past the working 30Hz refresh rate was picked.</li>
</ol>
<p>The PC could not be rebooted, with a single monitor, and it still worked.</p>
<h1 id="trying-to-setup-the-team-room-client">Trying to Setup the Team Room Client</h1>
<p>It is good that we can now use the Surface Hub for Teams calls again, but having to login to start Teams is not as nice as being able to invite the device as location to meetings.</p>
<p>To try to make the device a Teams Room we download the client from the <a href="https://teams.microsoft.com/l/message/48:notes/1761823413289?context=%7B%22contextType%22%3A%22chat%22%2C%22oid%22%3A%228%3Aorgid%3A7523db6f-265c-4c93-8633-81a9965e64a2%22%7D">Windows App Store</a> where it is still confusingly call the &lsquo;Skype Team Room&rsquo;. I guess it must be hard/impossible to change the name of a published package in the store. However, it is the correct one as the release history shows this app being recently updated and has the current Teams branding.</p>
<p>When you install and run this application it initially looks OK, but then fails saying</p>
<blockquote>
<p><strong>Microsoft Teams Room device isn&rsquo;t certified</strong></p>
<p>You must insert the HDMI adaptor that came with this device</p></blockquote>
<p>You have the option to restart the application, but you get the same result.</p>
<p>The bottom line is that this application seems to be locked down to certified hardware, there is no option for a DIY &lsquo;build your own unit&rsquo;, or at least not one I have been able to find thus far.</p>
<h1 id="where-we-are-now">Where we are now</h1>
<p>So now we have a working replacement PC connected to Surface Hub. This PC is domain joined, so staff can login for Teams meetings and use all the physical features of the Surface Hub. This is not a perfect replacement, but it at least means the Surface Hub is not just E-Waste.</p>
<p>I will post again, if we have any more successes</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why can&#39;t I see my Lenovo Thunderbolt 3 Dock?</title>
      <link>https://blog.richardfennell.net/posts/why-cant-i-see-my-thunderbolt-dock/</link>
      <pubDate>Thu, 31 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-cant-i-see-my-thunderbolt-dock/</guid>
      <description>&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;I recently rebuild my trusty Lenovo X1 Carbon laptop. As you usually find, a fresh install of Windows 11 meant a leap in performance.&lt;/p&gt;
&lt;p&gt;All was good for a couple of weeks, until I started to get problems. It still was working to supply power and the HDMI connection to my external monitor worked, but the USB devices could not be seen.&lt;/p&gt;
&lt;p&gt;I also checked with the &lt;a href=&#34;https://support.lenovo.com/us/en/solutions/nvid500262-lenovo-dock-manager-overview&#34;&gt;Lenovo Dock Manager&lt;/a&gt; and it could now no longer see the dock.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="the-problem">The Problem</h1>
<p>I recently rebuild my trusty Lenovo X1 Carbon laptop. As you usually find, a fresh install of Windows 11 meant a leap in performance.</p>
<p>All was good for a couple of weeks, until I started to get problems. It still was working to supply power and the HDMI connection to my external monitor worked, but the USB devices could not be seen.</p>
<p>I also checked with the <a href="https://support.lenovo.com/us/en/solutions/nvid500262-lenovo-dock-manager-overview">Lenovo Dock Manager</a> and it could now no longer see the dock.</p>
<h1 id="the-solution">The Solution</h1>
<p>I am not sure why it happened, but the fix was</p>
<ol>
<li>Open Device Manager: Press the Windows key + X and select &ldquo;Device Manager&rdquo;.</li>
<li>Show Hidden Devices: Click &ldquo;View&rdquo; in the menu bar and select &ldquo;Show hidden devices&rdquo;.</li>
<li>Locate Thunderbolt Controller: Expand &ldquo;System Devices&rdquo; and find the &ldquo;Thunderbolt Controller&rdquo; entry.</li>
<li>Uninstall: Right-click on &ldquo;Thunderbolt Controller&rdquo; and select &ldquo;Uninstall device&rdquo;. In the &ldquo;Uninstall Device&rdquo; window, check the box that says &ldquo;Delete the driver software for this device&rdquo;.</li>
</ol>
<p>Once this was done I disconnected the dock and reconnected it, and this dialog appeared</p>
<p><img alt="Dock Dialog" loading="lazy" src="/images/rfennell/dockfix.png"></p>
<p>I had to approve access to the dock.</p>
<p>My guess is that somehow this access control setting had got set in the wrong state. I have no idea how, as I was never shown this dialog after the Windows reinstall, but at least it is fixed now</p>
<blockquote>
<p><strong>Update 14th July 2026</strong></p>
<p>If this process does not work for you, or you cannot see the Thunderbolt device in the Windows Device Manager, try the following:</p>
<ol>
<li>Disconnect your laptop from the dock</li>
<li>Hold down the laptop power button until your laptop switches off</li>
<li>Wait a few seconds to make sure it is off</li>
<li>Switch on the laptop</li>
<li>When it is booted, connect the dock, there is a good chance all the device and displays connected to the dock will be working</li>
</ol>
<p>It seems the issue is that Windows 11 reboots/shutdown are not clearing some state that confuses USB-C/Thuderbolt connections. A hard reset of the electronics seems to fix it</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Creating a GitHub App based Azure DevOps Pipelines Service Connection</title>
      <link>https://blog.richardfennell.net/posts/setting-up-github-app-ado-service-connection-on-another-org/</link>
      <pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/setting-up-github-app-ado-service-connection-on-another-org/</guid>
      <description>&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/marketplace/azure-pipelines&#34;&gt;GitHub &amp;lsquo;Azure Pipelines&amp;rsquo; App&lt;/a&gt; enables some really interesting &amp;lsquo;better together&amp;rsquo; scenarios mixing the usage of Azure DevOps Pipelines for CI/CD processes while your source is stored on GitHub. It is particularly useful if an enterprise is migrating towards GitHub Enterprise over a period of time, maybe using &lt;a href=&#34;https://docs.github.com/en/enterprise-cloud@latest/migrations/using-github-enterprise-importer/migrating-from-azure-devops-to-github-enterprise-cloud/migrating-repositories-from-azure-devops-to-github-enterprise-cloud&#34;&gt;GitHub Enterprise Importer (GEI)&lt;/a&gt; to do the migration.&lt;/p&gt;
&lt;p&gt;I was recently working on such a migration for a client that involved numerous Azure DevOps organisations. In the past someone had setup the GitHub &amp;lsquo;Azure Pipelines&amp;rsquo; App on their GitHub organisation and got it working with one of their Azure DevOps organisation.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="the-problem">The Problem</h1>
<p>The <a href="https://github.com/marketplace/azure-pipelines">GitHub &lsquo;Azure Pipelines&rsquo; App</a> enables some really interesting &lsquo;better together&rsquo; scenarios mixing the usage of Azure DevOps Pipelines for CI/CD processes while your source is stored on GitHub. It is particularly useful if an enterprise is migrating towards GitHub Enterprise over a period of time, maybe using <a href="https://docs.github.com/en/enterprise-cloud@latest/migrations/using-github-enterprise-importer/migrating-from-azure-devops-to-github-enterprise-cloud/migrating-repositories-from-azure-devops-to-github-enterprise-cloud">GitHub Enterprise Importer (GEI)</a> to do the migration.</p>
<p>I was recently working on such a migration for a client that involved numerous Azure DevOps organisations. In the past someone had setup the GitHub &lsquo;Azure Pipelines&rsquo; App on their GitHub organisation and got it working with one of their Azure DevOps organisation.</p>
<p>The question was how do you setup a GitHub App based service connection in their other Azure DevOps organsations, without breaking what was already installed?</p>
<h1 id="service-connections--github-authentication">Service Connections &amp; GitHub Authentication</h1>
<p>The way Azure DevOps Pipelines communicate with GitHub is via a Service Connection.</p>
<p>To create a service connection you goto <code>Project Settings &gt; Pipelines &gt; Service Connections</code> then press the <code>New Service Connection</code> button, picking the service you require.</p>
<p>When you do this and pick the <code>GitHub</code> option you are offered two ways to authenticate</p>
<ul>
<li>A <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">PAT</a> - linked to a specific user and will expire after a fixed period</li>
<li>oAuth - again linked to a specific user, the one creating the service connection, but will not expire, so the preferred solution</li>
</ul>
<p>But neither of these are authentication via the <code>GitHub App</code>. So the question is how do you create a service connection of that type?</p>
<h1 id="solution">Solution</h1>
<p>It turns out adding the &lsquo;right type of service connection&rsquo; is easy, but really poorly documented. Every document seems to point to re-installing the Azure Pipelines App in GitHub, this is not required.</p>
<p>The process is as follows, assuming the Azure Pipeline GitHub App is installed and configured in your GitHub organisation.</p>
<ol>
<li>In a Team Project on the Azure DevOps organisation where you need the service connection create a new pipeline</li>
<li>Pick GitHub as the source location. You maybe prompted to authenticate, and then will be given a list of repositories to pick from, pick one, the choice of which one is not important.</li>
<li>Once this is done you are presented with a list of pipeline templates to use, at this point you can cancel the creation of the pipeline, completing it is not required.</li>
<li>If you look in <code>Project Settings &gt; Pipelines &gt; Service Connections</code> you will see a new Git Service Connection has been created with the name of the Azure DevOps Team Project that has the authentication method GitHub App, exactly what we require.</li>
</ol>
<p>In my scenario I was then able to rewire all the pipelines in my Azure DevOps organisation to point to GitHub using the GEI command <code>gh ado2gh share-service-connection</code> to share the new service connection to other Team Projects, and then <code>gh ado2gh rewire-pipeline</code> to change the source location.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Problems migrating Non-English Azure DevOps Servers to Azure DevOps Services and how to solve them</title>
      <link>https://blog.richardfennell.net/posts/problems-migrating-non-english-ado-server-to-azure/</link>
      <pubDate>Fri, 13 Jun 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/problems-migrating-non-english-ado-server-to-azure/</guid>
      <description>&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;Over the years, I have been involved in numerous migrations of TFS/Azure DevOps Server from on-premises servers to the cloud hosted Azure DevOps Services using the &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/devops/migrate/migration-overview?view=azure-devops#option-2-azure-devops-data-migration-tool&#34;&gt;Microsoft Azure DevOps Data Migration Tool&lt;/a&gt;. As long as you carefully followed the instructions, the process was relatively straightforward.&lt;/p&gt;
&lt;p&gt;I was recently involved in a cloud migration of a set of non-English Azure DevOps Servers i.e. the Azure DevOps Servers had been installed in French not English. Migrating these servers to Azure DevOps Services proved a challenge.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>Over the years, I have been involved in numerous migrations of TFS/Azure DevOps Server from on-premises servers to the cloud hosted Azure DevOps Services using the <a href="https://learn.microsoft.com/en-us/azure/devops/migrate/migration-overview?view=azure-devops#option-2-azure-devops-data-migration-tool">Microsoft Azure DevOps Data Migration Tool</a>. As long as you carefully followed the instructions, the process was relatively straightforward.</p>
<p>I was recently involved in a cloud migration of a set of non-English Azure DevOps Servers i.e. the Azure DevOps Servers had been installed in French not English. Migrating these servers to Azure DevOps Services proved a challenge.</p>
<h1 id="the-problem">The Problem</h1>
<p>The core of the problem with this type of migration is that the target system, Azure DevOps Services, is only available in English.</p>
<p>At this point it is important to explain what is meant by the previous statement. Azure DevOps Services allows you to set a language preference, which you set in your <a href="https://learn.microsoft.com/en-us/azure/devops/organizations/settings/set-your-preferences?view=azure-devops&amp;tabs=current-page">user profile</a>, but the underlying database is always in English. This means that if you have a non-English Azure DevOps Server, where the database is non-English, the migration will have problems.</p>
<p>This problem becomes apparent when you perform the first step of using the Azure DevOps Data Migration Tool, the <code>migrator validate</code> command to validate your TPC (Team Project Collection) prior to migration.</p>
<blockquote>
<p><strong>Note</strong> Remember you migrate a TPC, not a whole Azure DevOps Server, which could have many TPCs</p></blockquote>
<p>This command checks the source Azure DevOps Server TPC for issues that will block a migration. In the case of my Non-English server TPC the validation returned many issues.</p>
<p>The first was a warning about the database collation being <code>French_CI_AS</code>, so any migration could result in some characters not being migrated correctly e.g. Non-English characters like &lsquo;é&rsquo; might become equivalent to the English &rsquo;e&rsquo; after migration. However, this was just a warning, and not an error, so can be ignored for now and reviewed as part of the planned dry-run of the migration.</p>
<p>The bigger problem was the many errors in the form</p>
<pre tabindex="0"><code>VS403443: In order to migrate successfully, you must rename field &#39;System.Title&#39; to &#39;Title&#39;. Given name for &#39;System.Title&#39; is &#39;Titre&#39;
</code></pre><p>When you do these migrations there are often VSxxxxxx or TFxxxxxx errors, especially on TPCs that have been upgraded through various versions of TFS and Azure DevOps over the years. These are usually fixable by editing the process template <a href="https://learn.microsoft.com/en-us/azure/devops/migrate/migration-troubleshooting">as discussed in the migration documentation</a>.</p>
<p>However, when I tried to fix these issues I saw two completely separate sets of problems, depending on whether the TPC was using the older <a href="https://learn.microsoft.com/en-us/azure/devops/organizations/settings/work/inheritance-versus-hosted-xml?view=azure-devops">XML Process Templates or the newer Inherited Process Templates</a>.</p>
<h2 id="when-the-tpc-is-using-xml-process-templates">When the TPC is using XML Process Templates</h2>
<p>When the migration was for a TPC using the older XML Process templates, I found that once I had fixed the VS403443 errors using commands like</p>
<pre tabindex="0"><code>witadmin.exe changefield /collection:http://server:8080/tfs/defaultcollection, /n:System.Title /name:&#34;Title&#34; /noprompt
</code></pre><p>When I reran the <code>migrator validate</code> command, I got a new set of errors, but they were the same errors as before, just with the field names reversed e.g..</p>
<pre tabindex="0"><code>VS403443: In order to migrate successfully, you must rename field &#39;System.Title&#39; to &#39;Titre&#39;. Given name for &#39;System.Title&#39; is &#39;Title&#39;
</code></pre><p>I was in an endless loop of errors.</p>
<h2 id="when-the-tpc-is-using-inherited-process-templates">When the TPC is using Inherited Process Templates</h2>
<p>When the migration was for a TPC using the newer Inherited Process Templates, the problem was that I could not use the <code>witadmin</code> tool. There is in fact no supported way to update the display name for built-in work item types fields.</p>
<p>This is because the Inherited Process Templates are stored in the database, and not as XML files.</p>
<h1 id="the-solutions">The Solutions</h1>
<p>We have to consider the solutions to these problems separately.</p>
<h2 id="xml-process-templates">XML Process Templates</h2>
<p>After much unsuccessful experimentation, I reached out to the community to see if anyone had a solution to the problem and a fellow DevOps MVP, <a href="https://mvp.microsoft.com/en-US/MVP/profile/c183d172-3c9a-e411-93f2-9cb65495d3c4">Neno Loje</a> suggested he had had success in the past migrating TPCs originally setup in German, but the process he used came with some constraints.</p>
<p>The process I ended up with, based on his suggestions, was based around migrating the Non-English Team Project Collection (TPC) to an temporary English Azure DevOps Server:</p>
<ol>
<li>Build a new Azure DevOps Server, making sure you pick the default language English</li>
<li>Detach the TPC to be migrated on the Non-English Azure DevOps Server and make a SQL Backup of the detached TPC Database on the Non-English Azure DevOps Server
<blockquote>
<p><strong>Note</strong> Once the backup is done, you will usually reattach the TPC on the source Non-English server so users can continue to work while you do the dryrun migration</p></blockquote>
</li>
<li>Restore the TPC SQL backup on the new English Azure DevOps Server and attach the exported Non-English TPC to the new English Azure DevOps Server</li>
<li>On the new English Azure DevOps Server run the <code>migrator validate</code> command to check for errors, and fix them. This should be possible without getting into the error loops
<ul>
<li>
<p>Update all WIT fields causing the VS403443 errors, rename display names to the English names as discussed above using the <code>witadmin</code> command)</p>
<blockquote>
<p><strong>Note</strong> I wrote this <a href="https://gist.github.com/rfennell/e29cd9250de0eb955c55373721b9f786">PowerShell script</a> to automate the process of generating the commands to do the renaming of fields in a process template based on the migration error log.</p></blockquote>
</li>
<li>
<p>Fixing any remaining errors in the migration should be possible with further XML template file edits which are published using the <code>witadmin</code> command as discussed in the <a href="https://learn.microsoft.com/en-us/azure/devops/migrate/migration-troubleshooting">migration documentation</a></p>
<blockquote>
<p><strong>Note</strong> Watch out for any <code>for=[group]</code> and <code>not=[group]</code> attributes in the WITD XML files, used to conditionally set field constraints e.g. make a field readonly for all users except those in a specific group. These constraints are not supported in Azure DevOps Services, but are not always picked up by <code>migrator validate</code>. If they are not removed, they will cause the import to fail. An import failure email message will be recieved via email in form</p>
<p><code>Step : ProcessValidation - Failure Type - Validation failed : Invalid process template: WorkItem Tracking\TypeDefinitions\Bug.xml:: Object reference not set to an instance of an object.</code></p></blockquote>
</li>
<li>
<p>You can also look at using the <a href="https://github.com/microsoft/process-customization-scripts">ConformProject.ps1</a> script to help with this process. ConformProject will take a defined process template that is on your local machine and apply it, in full, to a specified project. This is a good way to get rid of all the errors, but you will lose all your customisations, so use with care.</p>
</li>
</ul>
</li>
<li>Once all the errors are fixed, the validation should pass, and hence the rest of the migration process should be possible</li>
</ol>
<p>However, the fact your migration completed might not be the end of your issues. There is of course the potential issue of the collation of the database, but arguably this is a minor issue. The more important point is something Neno had mentioned. He commented that this process is good for a read-only migration to Azure DevOps Services, but if you wish to continue using the migrated work items more work is required.</p>
<p>The reason for this is not the migration of the process template, it&rsquo;s the actual work item data. The imported work items are still using strings for state and other fields in the non-English language and need to be &ldquo;converted&rdquo; to values that match the constraints of the &lsquo;fixed&rsquo; english process template.</p>
<p>To fix this means edits are required for all active work items. This of course could be done one-by-one as Work Items are edited, or via scripts to make the changes at scale, but more the work will be required to make the work item data usable in Azure DevOps Services.</p>
<h2 id="inherited-process-templates">Inherited Process Templates</h2>
<blockquote>
<p><strong>&mdash;&mdash; DANGER ZONE &mdash;&mdash;</strong></p>
<p>The following section describes a process that is <strong>not supported</strong> by Microsoft and could lead to serious issues with your Azure DevOps Server.</p>
<p>The process should only be used on a copy of a system, <strong>not on a live system</strong> itself.</p>
<p><strong>I accept no responsibility</strong> for any issues that arise from following this process, you follow it at your own risk.</p></blockquote>
<p>When the migration was for a TPC using Inherited Process Templates, the solution is very different, and can only be described as a <strong>massive hack</strong>.</p>
<p>There are two reasons for the need for a different process</p>
<ul>
<li>With inherited process templates the built-in work item types are by design read only. Your customisations are additions to the built-in work item types. You cannot change the built-in work item types, you can only hide or add new fields to them. There is no equivalent of the <code>witadmin</code> command to change the display names of fields in the Inherited Process Templates, all changes are done via the Azure DevOps UI or REST API.</li>
<li>The other reason for the different process is that it is not possible to attach a Non-English Inherited Process TPC to an English Azure DevOps Server. Any &lsquo;fixing&rsquo; process must be done on a Non-English server.</li>
</ul>
<p>So, the process I ended up with is as follows:</p>
<ol>
<li><strong>IMPORTANT</strong> Create a duplicate of your Non-English Azure DevOps Server, this is the one you will use for the migration process. This step is very important, as you will be making SQL edits to the database which is very unsupported.</li>
<li>On the duplicate Non-English Azure DevOps Server load <a href="https://learn.microsoft.com/en-us/ssms/install/install">SQL Server Management Studio (SSMS)</a> and connect to the TPC database.</li>
<li>For each VS403443 error run the following SQL command to update the display name of the field in the database:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="p">[</span><span class="n">dbo</span><span class="p">].[</span><span class="n">WorkItemTypeField</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="p">[</span><span class="n">Name</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Title&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">[</span><span class="n">ReportingName</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Title&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="p">[</span><span class="n">ReferenceName</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;System.Title&#39;</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p><strong>Note</strong> Again I wrote a <a href="https://gist.github.com/rfennell/3fb21ce54d9baed33fcca0465f933b51">PowerShell script</a> to automate the process of generating the SQL commands to do the renaming of fields in a process template based on the migration error log.</p></blockquote>
</li>
<li>Rerun the <code>migrator validate</code> command to check for any remaining errors, and fix them.</li>
<li>Once all the errors are fixed, the validation should pass, and hence the rest of the migration process should be possible</li>
</ol>
<p>Again the fact your migration completed is not the end of your issues, in fact the issues are much worse than with the XML Process Templates.</p>
<p>The reason for this is that the Non-English inherited process template tries to match all your work items to the built-in work item types based on the string of the work item type name. So for example, it will try to match the work item type &lsquo;Bogue&rsquo; in your French team project to the built-in work item type &lsquo;Bug&rsquo;, which will of course fail.</p>
<p>This is a serious problem, as it means much of the Azure Boards and Azure Test Plans UI will fail to load any work items, if they even load at all. The work item data is there, you can see it via the REST API e.g. <code>https://dev.azure.com/{organization}/{project}/_apis/wit/workitems/{id}</code>, but the UI will not work as the work item type names are not what the English based Azure DevOps Services instance is expecting.</p>
<p>In theory there must be a way to fix this, but it will require a lot of SQL edits prior to migration to Azure. Far more work than I have the appetite to do.</p>
<p>So in summary, you have to consider this process for migrating Non-English inherited process based TPCs to only be acceptable if your key goal is to migrate your TFVC repositories with no data loss to Azure DevOps Services, and that you are prepared to accept that the work item data will not be usable.</p>
<p>Your only real option if you need the work item data from a Non-English on-premises Inherited process TPC in Azure DevOps Services is to look at using <a href="https://github.com/nkdAgility/azure-devops-migration-tools">Martin Hinshelwood Azure DevOps Migration Tools</a> to migrate the on-premises work items to a new Azure DevOps Services project on the migrated Azure DevOps Service instance, one that is setup using an English based inherited process template. Given the need to address translation issues during this migration this will also be a not insignificant undertaking.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Migrating a non-English Azure DevOps Server to Azure DevOps Services is possible, but it is <strong>not officially supported</strong> and requires some extra steps, some of them <strong>very &lsquo;hacky&rsquo;</strong>.</p>
<p>If you are considering either of the migration paths in this post, I would recommend in the strongest terms that you perform your migrations  on a duplicate of your Azure DevOps Server and not on your live system.</p>
<p>I cannot stress enough, please <strong>DO NOT</strong> make SQL edits of your live Azure DevOps Server database, as this is not supported and could lead to serious issues with your live system.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using GitHub Copilot to write an Azure DevOps Test Plan Export Tool</title>
      <link>https://blog.richardfennell.net/posts/using-copilot-to-write-an-azure-devops-export-tool/</link>
      <pubDate>Wed, 28 May 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-copilot-to-write-an-azure-devops-export-tool/</guid>
      <description>&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;I got asked today by a client if there was a way to automate the exporting Azure DevOps Test Plans to Excel files. They knew they could do it manually via the Azure DevOps UI, but had a lot of Test Plans to export and wanted to automate the process.&lt;/p&gt;
&lt;h1 id=&#34;the-options&#34;&gt;The Options&lt;/h1&gt;
&lt;p&gt;I considered a few options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://learn.microsoft.com/en-us/azure/devops/test/test-case-managment-reference?view=azure-devops&#34;&gt;TCM CLI&lt;/a&gt; - This is a command line tool that can be used to interact with Azure DevOps Test Plans. It can be used to import or clone Test Plans, but not to export them.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://learn.microsoft.com/en-us/cli/azure/devops?view=azure-cli-latest&#34;&gt;AZ DEVOPS CLI&lt;/a&gt; - This is a command line tool that can be used to interact with Azure DevOps. Unfortunately, it does not have any commands to export Test Plans.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.2&#34;&gt;Azure DevOps REST API&lt;/a&gt; - This is a powerful API that can be used to interact with Azure DevOps, but the documentation makes no mention of a call to export Test Plans.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But&amp;hellip;..&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>I got asked today by a client if there was a way to automate the exporting Azure DevOps Test Plans to Excel files. They knew they could do it manually via the Azure DevOps UI, but had a lot of Test Plans to export and wanted to automate the process.</p>
<h1 id="the-options">The Options</h1>
<p>I considered a few options:</p>
<ul>
<li><a href="https://learn.microsoft.com/en-us/azure/devops/test/test-case-managment-reference?view=azure-devops">TCM CLI</a> - This is a command line tool that can be used to interact with Azure DevOps Test Plans. It can be used to import or clone Test Plans, but not to export them.</li>
<li><a href="https://learn.microsoft.com/en-us/cli/azure/devops?view=azure-cli-latest">AZ DEVOPS CLI</a> - This is a command line tool that can be used to interact with Azure DevOps. Unfortunately, it does not have any commands to export Test Plans.</li>
<li><a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.2">Azure DevOps REST API</a> - This is a powerful API that can be used to interact with Azure DevOps, but the documentation makes no mention of a call to export Test Plans.</li>
</ul>
<p>But&hellip;..</p>
<h1 id="the-solution">The Solution</h1>
<p>I used a process I have used before to write tools for Azure DevOps. I opened the page in the Azure DevOps UI that I wanted to automate, and then used the browser developer tools to inspect the network traffic. I found a call that was made to export the Test Plan to Excel, and then used that as the basis for my tool.</p>
<p>Turns out there is an undocumented API call that can be used to export Test Plans to Excel. A POST call is made to the following URL:</p>
<pre tabindex="0"><code>https://dev.azure.com/{organization}/{project}/_apis/testplan/TestCases/TestCaseFile?api-version=7.1-preview.1
</code></pre><p>with a payload that looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;testPlanId&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;testSuiteId&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;testCaseIds&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;columnOptions&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;System.Id&#34;</span><span class="p">,</span><span class="s2">&#34;System.AssignedTo&#34;</span><span class="p">,</span><span class="s2">&#34;System.State&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h1 id="the-code">The Code</h1>
<p>So I needed to write a tool get all the test plans and test suites in an Azure DevOps Team project, and then call the API to export them to a set of Excel files.</p>
<p>I usually write this type of tool in PowerShell, but try as I might, even though I appeared to get the expected data back from the API call, I could not write it to disk in such a way Excel could read it. Excel said the file was corrupt.</p>
<p>As I had delivered a webinar today on using GitHub Copilot to write code, I thought I should practice what I preach and use it to fix the tool. As I suspected the issue was not the API call, but the PowerShell encoding of the data, I decided to ask Copilot to convert the code to Python, a language I am not too familiar with.</p>
<p>So with a couple of prompts my simple PowerShell proof of concept was converted to a Python script, and as I had hoped the resultant saved file loaded in Excel without a problem.</p>
<p>It was then a simple matter of adding more code, via Copilot prompts, to loop through all the Test Plans and Test Suites in the Team Project, and then call the API to export them to a set of Excel files.</p>
<p>This is not to say I did not have to make some tweaks to the code, but Copilot got most of it correct, there were a couple of edits I had to make to API URL strings, but Copilot certainly wrote the Python code much faster than I could have done myself.</p>
<p>So certainly a win for GitHub Copilot, and I now have a tool that can be used to export Azure DevOps Test Plans to Excel files at scale.</p>
<script src="https://gist.github.com/rfennell/43f0746e93d57de55ea5989dc213fc2d.js"></script>
]]></content:encoded>
    </item>
    <item>
      <title>Experiences Migrating CI/CD solutions with GitHub Copilot</title>
      <link>https://blog.richardfennell.net/posts/experiences-migrating-cicd-with-copilot/</link>
      <pubDate>Fri, 09 May 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/experiences-migrating-cicd-with-copilot/</guid>
      <description>&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I have recently been swapping some Azure DevOps Pipelines to GitHub Actions as part of a large GitHub Enterprise migration. The primary tool I have been using for this is GitHub Copilot in the new &lt;a href=&#34;https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode&#34;&gt;Agent Mode&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Frankly, Copilot is like magic, it is amazing how close it gets to a valid solution. I say this after suffering years of undelivered marketing promises of no-code &amp;ldquo;we won&amp;rsquo;t need developers in the future&amp;rdquo;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>I have recently been swapping some Azure DevOps Pipelines to GitHub Actions as part of a large GitHub Enterprise migration. The primary tool I have been using for this is GitHub Copilot in the new <a href="https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode">Agent Mode</a></p>
<p>Frankly, Copilot is like magic, it is amazing how close it gets to a valid solution. I say this after suffering years of undelivered marketing promises of no-code &ldquo;we won&rsquo;t need developers in the future&rdquo;.</p>
<p>Thats said, I have yet to find a Copilot translated GitHub Action workflow that works 100% first time. Interestingly they often run without error, but don&rsquo;t have the desired results of the source pipeline.</p>
<h2 id="so-what-have-i-learnt">So what have I learnt?</h2>
<h3 id="your-prompts-will-not-be-as-definitive-as-you-think">Your prompts will not be as &lsquo;definitive&rsquo; as you think.</h3>
<p>Many steps done using Azure DevOps tasks have no obvious 1-to-1 equivalent in GitHub Actions, so Copilot generates script blocks to perform the same function.</p>
<p>I was moving an Azure DevOps YAML pipeline that was targeting Ubuntu to GitHub Actions, so not unreasonably the generate scripts were in BASH. The problem for me was that I don&rsquo;t have that much BASH experience, so making my debugging slower.</p>
<p>The solution in my case was as simple as to add an initial prompt stating any generated scripts should be in PowerShell Core, or as I did, use subsequent prompts to alter the generated scripts.</p>
<blockquote>
<p><strong>Note</strong> If you always want given prompt to be present, such as to favour PowerShell in a given repo, the you could look at adding a <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot">.github/copilot-instructions.md</a> file to define prompts that are added to everything.</p></blockquote>
<h3 id="silent-errors-are-the-root-of-many-issues">Silent errors are the root of many issues</h3>
<p>Most of the problems I had with migrated pipelines were the ones you experience in any CI/CD solution i.e. you are in the wrong folder, a filter is wrong, the built URI is incorrect etc.</p>
<p>Using Copilot for this migration, these types of errors seemed to be caused by either</p>
<ul>
<li>the move from Azure DevOps tasks, that may have hidden behaviors or defaults, to scripts</li>
<li>or the differences in CLI tools called between the tasks and generated scripts</li>
</ul>
<p>I good example was the way an archive/ZIP step worked. I ended up creating a ZIP file of static website content with an extra root folder, so all other folders were one level deeper than expected. This deployed without error, but obviously did not work as the old site content had not been overwritten, all the new content was in a sub-folder. A simple problem, but one that took me too lon to realised what the cause was as I got no error messages.</p>
<h2 id="summary">Summary</h2>
<p>GitHub Copilot can get you 90% plus of the way to a solution, but as with all coding the &lsquo;devil is in the detail&rsquo;. That last few percent will take a disproportionate time.</p>
<p>Also there is the danger it generates code you don&rsquo;t understand, but that is a learning opportunity is it not?</p>
<p>That said, with careful crafting of your prompts you get a better solution faster, as long as you don&rsquo;t relax the quality checks you should be doing for any solution whether human or AI created.</p>
]]></content:encoded>
    </item>
    <item>
      <title>You need to wait longer when restarting Logic Apps</title>
      <link>https://blog.richardfennell.net/posts/you-need-to-wait-longer-when-restarting-logic-apps/</link>
      <pubDate>Tue, 25 Mar 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/you-need-to-wait-longer-when-restarting-logic-apps/</guid>
      <description>&lt;p&gt;I have been doing some work one a Logic Apps that routes its traffic out via a vNet and accesses its underlying &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/storage/common/storage-private-endpoints&#34;&gt;Storage Account via private endpoints&lt;/a&gt;. &amp;lsquo;Nothing that special in that configuration&amp;rsquo; I hear you saying, but I did manage to confuse myself whilst testing.&lt;/p&gt;
&lt;p&gt;After changing my configuration of the vNet, I restarted the Logic App to make sure everything was working as expected on startup.&lt;/p&gt;
&lt;p&gt;The Logic App appeared to start within a few seconds, the Overview pane in the Azure Portal showing the correct values.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have been doing some work one a Logic Apps that routes its traffic out via a vNet and accesses its underlying <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-private-endpoints">Storage Account via private endpoints</a>. &lsquo;Nothing that special in that configuration&rsquo; I hear you saying, but I did manage to confuse myself whilst testing.</p>
<p>After changing my configuration of the vNet, I restarted the Logic App to make sure everything was working as expected on startup.</p>
<p>The Logic App appeared to start within a few seconds, the Overview pane in the Azure Portal showing the correct values.</p>
<p>However, if I tried to access any of the workflows in the Logic App, I was presented with a &lsquo;bad gateway&rsquo; error. On receiving this error, I assumed that I had made a mistake in the configuration of the vNet and started to investigate.</p>
<p>However, I should not have been so hasty. After waiting a minute or two, the Logic App started to work as expected. It appears that the Logic App Overview pane is updated long before the Logic App is fully operational,so can&rsquo;t be trusted as a source of truth.</p>
<p>So not a revelatory blog post, but a reminder to myself to be patient when restarting Logic Apps.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why cannot I see my Logic App action output?</title>
      <link>https://blog.richardfennell.net/posts/why-cant-i-see-my-logic-app-action-output/</link>
      <pubDate>Sat, 15 Mar 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-cant-i-see-my-logic-app-action-output/</guid>
      <description>&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;I recently was debugging an &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/logic-apps/add-run-powershell-scripts#:~:text=To%20perform%20custom%20integration%20tasks%20inline%20with%20your,the%20Inline%20Code%20action%20named%20Execute%20PowerShell%20Code.&#34;&gt;Execute PowerShell Code&lt;/a&gt; Logic App action, so wanted to see it&amp;rsquo;s output. However, when I reviewed the run history, the output for my code action was empty.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;No Output&#34; loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/images/rfennell/logic-app-actions-output-bad.png&#34;&gt;&lt;/p&gt;
&lt;h1 id=&#34;the-cause-and-solution&#34;&gt;The Cause (and Solution)&lt;/h1&gt;
&lt;p&gt;The problem turned out to be that the Logic App&amp;rsquo;s Inbound traffic configuration. You can replicate this issue easily&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new Logic App Workflow, use any trigger and then add an &lt;code&gt;Execute PowerShell Code&lt;/code&gt; action, you don&amp;rsquo;t need to edit the default code sample.&lt;/li&gt;
&lt;li&gt;Save the new Action and then run it.&lt;/li&gt;
&lt;li&gt;Review the run history and you&amp;rsquo;ll see that the output as expected.
&lt;img alt=&#34;Output Shown&#34; loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/images/rfennell/logic-app-actions-output-good.png&#34;&gt;&lt;/li&gt;
&lt;li&gt;Next, change the Logic App&amp;rsquo;s &amp;lsquo;Inbound traffic configuration &amp;gt; Public Network Access&amp;rsquo; to &amp;lsquo;Disabled&amp;rsquo; and run the Logic App again, and the output now cannot be seen, as shown above.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You are actually warned about this in the Azure Portal the first time you view a run history the settings that block it&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="the-problem">The Problem</h1>
<p>I recently was debugging an <a href="https://learn.microsoft.com/en-us/azure/logic-apps/add-run-powershell-scripts#:~:text=To%20perform%20custom%20integration%20tasks%20inline%20with%20your,the%20Inline%20Code%20action%20named%20Execute%20PowerShell%20Code.">Execute PowerShell Code</a> Logic App action, so wanted to see it&rsquo;s output. However, when I reviewed the run history, the output for my code action was empty.</p>
<p><img alt="No Output" loading="lazy" src="/images/rfennell/logic-app-actions-output-bad.png"></p>
<h1 id="the-cause-and-solution">The Cause (and Solution)</h1>
<p>The problem turned out to be that the Logic App&rsquo;s Inbound traffic configuration. You can replicate this issue easily</p>
<ol>
<li>Create a new Logic App Workflow, use any trigger and then add an <code>Execute PowerShell Code</code> action, you don&rsquo;t need to edit the default code sample.</li>
<li>Save the new Action and then run it.</li>
<li>Review the run history and you&rsquo;ll see that the output as expected.
<img alt="Output Shown" loading="lazy" src="/images/rfennell/logic-app-actions-output-good.png"></li>
<li>Next, change the Logic App&rsquo;s &lsquo;Inbound traffic configuration &gt; Public Network Access&rsquo; to &lsquo;Disabled&rsquo; and run the Logic App again, and the output now cannot be seen, as shown above.</li>
</ol>
<p>You are actually warned about this in the Azure Portal the first time you view a run history the settings that block it</p>
<p><img alt="No Output Message" loading="lazy" src="/images/rfennell/logic-app-actions-output-msg.png"></p>
<p>But it is easy to miss, or as I did, just forget about the limitation.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Parsing CSV files in Azure Logic Apps</title>
      <link>https://blog.richardfennell.net/posts/parsing-csv-file-in-logic-apps/</link>
      <pubDate>Wed, 05 Mar 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/parsing-csv-file-in-logic-apps/</guid>
      <description>&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;I recently had the need in an Azure Logic App to read a CSV file from an Azure Storage account, parse the file, and then process the data row by row.&lt;/p&gt;
&lt;p&gt;Unfortunately, there is no built-in action in Logic Apps to parse CSV files.&lt;/p&gt;
&lt;p&gt;So as to avoid having to write an Azure function, or use a number of slow, low level Logic App actions, I decided to use the &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/logic-apps/add-run-powershell-scripts&#34;&gt;PowershellCode&lt;/a&gt; action to parse the CSV file quickly inline with the rest of the Logic App.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="the-problem">The Problem</h1>
<p>I recently had the need in an Azure Logic App to read a CSV file from an Azure Storage account, parse the file, and then process the data row by row.</p>
<p>Unfortunately, there is no built-in action in Logic Apps to parse CSV files.</p>
<p>So as to avoid having to write an Azure function, or use a number of slow, low level Logic App actions, I decided to use the <a href="https://learn.microsoft.com/en-us/azure/logic-apps/add-run-powershell-scripts">PowershellCode</a> action to parse the CSV file quickly inline with the rest of the Logic App.</p>
<p>The documented code sample suggests the following should work</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># Retrieve outputs from prior steps</span>
</span></span><span class="line"><span class="cl"><span class="nv">$csvdata</span> <span class="p">=</span> <span class="nb">Get-ActionOutput</span> <span class="n">-ActionName</span> <span class="n">FileContent</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$rows</span> <span class="p">=</span> <span class="nv">$csvdata</span> <span class="p">|</span> <span class="nb">ConvertFrom-csv</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Push outputs forward to subsequent actions</span>
</span></span><span class="line"><span class="cl"><span class="nb">Push-WorkflowOutput</span> <span class="n">-Output</span> <span class="nv">$rows</span>
</span></span></code></pre></div><p>However this resulted in a <code>null</code> value for <code>$rows</code>.</p>
<h1 id="the-solution">The Solution</h1>
<p>The problem turned out to be that the <code>getFileContentV2</code> action that I used to load the file from Azure storage, returned the file content as part of an object, not as a simple string. So I had to request the correct property from the object.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># Retrieve outputs from prior steps</span>
</span></span><span class="line"><span class="cl"><span class="nv">$action</span> <span class="p">=</span> <span class="nb">Get-ActionOutput</span> <span class="n">-ActionName</span> <span class="n">FileContent</span>
</span></span><span class="line"><span class="cl"><span class="nv">$csvdata</span> <span class="p">=</span> <span class="nv">$action</span><span class="p">.</span><span class="n">outputs</span><span class="p">[</span><span class="s1">&#39;body&#39;</span><span class="p">].</span><span class="py">ToString</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$rows</span> <span class="p">=</span> <span class="nv">$csvdata</span> <span class="p">|</span> <span class="nb">ConvertFrom-csv</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Push outputs forward to subsequent actions</span>
</span></span><span class="line"><span class="cl"><span class="nb">Push-WorkflowOutput</span> <span class="n">-Output</span> <span class="nv">$rows</span>
</span></span></code></pre></div><p>Once this change was made, the CSV file was parsed correctly and I was able to process the data as required.</p>
]]></content:encoded>
    </item>
    <item>
      <title>I know I am late to the game with Home Assistant....</title>
      <link>https://blog.richardfennell.net/posts/late-to-the-game-with-homeassistant/</link>
      <pubDate>Thu, 20 Feb 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/late-to-the-game-with-homeassistant/</guid>
      <description>&lt;p&gt;Since moving house a couple of years ago, I have gained an ever increasing set of apps on my phone to manage various things, such the &lt;a href=&#34;https://www.solaredge.com/uk/home/solaredge-home&#34;&gt;Solaredge&lt;/a&gt; PV and battery system we installed and our &lt;a href=&#34;https://share.octopus.energy/fawn-may-441&#34;&gt;Octopus Energy&lt;/a&gt; account, to name but two.&lt;/p&gt;
&lt;p&gt;In the past, I had avoided IOT and the &amp;lsquo;Intelligent Home&amp;rsquo; as I did not want to get to the point where &amp;lsquo;I can&amp;rsquo;t do the washing up, the Internet is down&amp;rsquo;. However, I recently got sent an &lt;a href=&#34;https://octopus.energy/blog/octopus-home-mini/&#34;&gt;Octopus Mini&lt;/a&gt; which meant for the first time I could get real time information from their API, so no longer needing to wait until my Smart Meter got around to transferring data to them, which could be up to 48 hours later.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Since moving house a couple of years ago, I have gained an ever increasing set of apps on my phone to manage various things, such the <a href="https://www.solaredge.com/uk/home/solaredge-home">Solaredge</a> PV and battery system we installed and our <a href="https://share.octopus.energy/fawn-may-441">Octopus Energy</a> account, to name but two.</p>
<p>In the past, I had avoided IOT and the &lsquo;Intelligent Home&rsquo; as I did not want to get to the point where &lsquo;I can&rsquo;t do the washing up, the Internet is down&rsquo;. However, I recently got sent an <a href="https://octopus.energy/blog/octopus-home-mini/">Octopus Mini</a> which meant for the first time I could get real time information from their API, so no longer needing to wait until my Smart Meter got around to transferring data to them, which could be up to 48 hours later.</p>
<p>So I thought it time to look at unifying the management of all these systems, and potentially automating some behaviors such as switching things on or off based the current cost of power and the performance of our PV/Battery system.</p>
<p>So enter <a href="https://www.home-assistant.io/">Home Assistant</a>, an OSS project I had heard very good things about, and I have to say lived up to the hype.</p>
<p><img alt="Home Assistant" loading="lazy" src="/images/rfennell/ha-dashboard.png"></p>
<p>The <a href="https://www.home-assistant.io/installation/generic-x86-64/">installation was very easy</a>, I used an old Intel NUC PC I had lying around.</p>
<p>Once running on my home network, Home Assistant automatically detected far more devices than I had expected. These included an LG TV, AudioCast devices, a old NetGear NAS, and a good few more. Very impressive, using only &lsquo;standard device integrations&rsquo; shipped in the basic Home Assistant install, and no additional configuration by me.</p>
<p>Connecting to Octopus Energy and Solaredge took a bit more work as these integration were not shipped as standard.</p>
<p>First, I installed <a href="https://hacs.xyz/">Home Assistant Community Store (HACS)</a>. This allows you to easily install any community Home Assistant add-in hosted GitHub. Once this was in place I could install community integration for <a href="https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy">Octopus Energy</a> and <a href="https://github.com/binsentsu/home-assistant-solaredge-modbus">Solaredge Modbus</a>.</p>
<blockquote>
<p><strong>Note</strong> There is a <a href="https://www.home-assistant.io/integrations/solaredge">Solaredge</a> device integration shipped with Home Assistant, but it queries the Solaredge web-hosted API and not the local device, so any data is 15-30 minutes out of date. The community Modbus version queries the Solaredge inverter directly over Ethernet, so provides real time data. Much for useful if planning to do home automation based on current conditions.</p></blockquote>
<p>Again, this was all very easy, a few clicks and they were installed. The only complexity, where I had to do some reading and learning, was getting the correct data for Home Assistant Energy Dashboard (shown above). This was because the Solaredge Modbus integration provides a lot of low level raw data, but not all the values the Energy Dashboard needs.</p>
<p>However, as I was learning to expect for Home Assistant, the solution was well documented in the <a href="https://github.com/binsentsu/home-assistant-solaredge-modbus/wiki/Using-Templated-Sensors-to-Calculate-Power-Flow-and-Energy">Solaredge Modbus integration GitHub Project WIKI</a>. To use <a href="https://www.home-assistant.io/integrations/template/">Sensor Templates</a> to process the raw data to the forms needed. With hindsight, having to work my way though configuring this was an excellent way to learn Home Assistant better. I find it is always better to learn by trying to fix a problem you are experiencing, rather than just following tutorials</p>
<p>So, my Home Assistant is up and running. It is great to see such a successful and well documented OSS project with a large community of contributors. I am looking forward to seeing what else I can do with Home Assistant, but still fear I am on the slippery slope to being &lsquo;unable to do the washing up due to an Internet outage&rsquo;.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What are my options authenticating the az devops CLI?</title>
      <link>https://blog.richardfennell.net/posts/what-are-my-options-authenticating-az-devops-api/</link>
      <pubDate>Fri, 31 Jan 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/what-are-my-options-authenticating-az-devops-api/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Edited:&lt;/strong&gt; 4th Feb 2025 to add detail on Azure DevOps Pipelines Service Connection&lt;/p&gt;&lt;/blockquote&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;When automating administration tasks via scripts in Azure DevOps in the past I would have commonly used the &lt;a href=&#34;https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.2&#34;&gt;Azure DevOps REST API&lt;/a&gt;. However, today I tend to favour the &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/devops/cli/?view=azure-devops&#34;&gt;Azure DevOps CLI&lt;/a&gt;. The reason for this is that the CLI wrappers the REST API in such a way that it is easier to use and more consistent.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>Edited:</strong> 4th Feb 2025 to add detail on Azure DevOps Pipelines Service Connection</p></blockquote>
<h1 id="introduction">Introduction</h1>
<p>When automating administration tasks via scripts in Azure DevOps in the past I would have commonly used the <a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.2">Azure DevOps REST API</a>. However, today I tend to favour the <a href="https://learn.microsoft.com/en-us/azure/devops/cli/?view=azure-devops">Azure DevOps CLI</a>. The reason for this is that the CLI wrappers the REST API in such a way that it is easier to use and more consistent.</p>
<p>One of the most noticeable advantages is in the area of authentication. The CLI supports a number of authentication methods that are not directly available to the REST API.</p>
<p>In this post, I will explore the options available to authenticate the Azure DevOps CLI.</p>
<h1 id="authentication-options">Authentication Options</h1>
<p>The Azure CLI has a login command <code>az login</code>, but so does the Azure DevOps extension <code>az devops login</code>. We have the option to use either depending on what we are are trying to do.</p>
<p>The key thing to considers is scope, whether you are trying to autenticate with the whole an Azure subscription or just an Azure DevOps instance.</p>
<h2 id="azure-cli-personal-access-token-pat">Azure CLI Personal Access Token (PAT)</h2>
<p><a href="https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&amp;tabs=Windows">PATs</a> have historically been the usual means to authenticate with Azure DevOps . They are still perfectly valid, and the easiest way to authenticate for command line usage. However, it is worth noting that <a href="https://devblogs.microsoft.com/devops/reducing-pat-usage-across-azure-devops/">Microsoft are now recommending a move away from PATs</a>.</p>
<p>The basic command to use a PAT is <code>az devops login</code>, when this is run you will be prompted for your PAT.</p>
<p>A script can build on this authentication mechanism and allow the PAT to be passed into the script by echoing it into the <code>az</code> command.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">echo </span> <span class="s2">&#34;&lt;my-pat&gt;&#34;</span> <span class="p">|</span> <span class="n">az</span> <span class="n">devops</span> <span class="n">login</span> <span class="p">-</span><span class="n">-organization</span> <span class="s2">&#34;https://dev.azure.com/myorg
</span></span></span></code></pre></div><h2 id="interactive-login-using-entra-id">Interactive Login using Entra ID</h2>
<p>An alternative to using a PAT is to login to the Azure CLI using your corporate ID that has access to your Azure Subscription. This can be done outside of any script by using the <code>az login</code> command prior to running the script, or making this command the first one in the script.</p>
<p>When the <code>az login</code> command is run, if it can, it will launch a browser allowing you to authenticate via Entra ID using your corporate ID. Once done, your session will be valid for a period of time, and you can use any <code>az</code> commands.</p>
<p>If a browser is not available, the <code>az login</code> command initiates the device code flow and instructs the user to open a browser page at <a href="https://aka.ms/devicelogin">https://aka.ms/devicelogin</a> on another device. Then, enter the code displayed in the terminal. This flow can be forced by using the command</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"> <span class="n">az</span> <span class="n">login</span> <span class="p">-</span><span class="n">-use-device-code</span>
</span></span></code></pre></div><p>The problem both those mechanisms that they are not suitable for a script that needs to run unattended, as they requires a user to be present to authenticate.</p>
<h2 id="using-a-service-principle">Using a Service Principle</h2>
<p>A <a href="https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops">Service Principle</a> is an application registration within Entra ID.</p>
<p>In Azure DevOps, a Service Principle can be granted access to an Azure DevOps Organisation and Projects as you would a user account i.e. the Service Account can be granted a license and given permissions inside a project.</p>
<p>You can create a Service Principle using the command</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">ad</span> <span class="nb">sp create-for</span><span class="n">-rbac</span> <span class="p">-</span><span class="n">-name</span> <span class="n">myServicePrincipalName1</span> <span class="p">-</span><span class="n">-role</span> <span class="n">reader</span> <span class="p">-</span><span class="n">-scopes</span> <span class="p">/</span><span class="n">subscriptions</span><span class="p">/</span><span class="mf">00000000</span><span class="p">-</span><span class="mf">0000</span><span class="p">-</span><span class="mf">0000</span><span class="p">-</span><span class="mf">0000</span><span class="p">-</span><span class="mf">000000000000</span> 
</span></span></code></pre></div><blockquote>
<p><strong>Note:</strong> The Service Principle must have at least the role of &lsquo;reader&rsquo; in the Azure Subscription associated with the Entra ID</p></blockquote>
<p>When the above command is run, the result will be block of JSON including an appId, password and tenantID. These values should be stored securely</p>
<p>Once the Service Principle has been created you can grant it permissions to the Azure DevOps Organisation and Projects as needed.</p>
<p>Finally, using the securely stored values, you can now authenticate with the Azure ClI using the form</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">login</span> <span class="p">-</span><span class="n">-service-principal</span> <span class="n">-u</span> <span class="p">&lt;</span><span class="n">appID</span><span class="p">&gt;</span> <span class="n">-p</span> <span class="p">&lt;</span><span class="n">password</span><span class="p">&gt;</span> <span class="p">-</span><span class="n">-tenant</span> <span class="p">&lt;</span><span class="n">tenantid</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Unlike the interactive login, if you use this mechanism you have the option to pass these values into a script as parameters. So can be used for unattended scripts.</p>
<p>The issue with this mechanism is that the password will expire, and has to be renewed. So there is a maintainance overhead.</p>
<h2 id="managed-identity">Managed Identity</h2>
<p>The final option is to use an <a href="https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?toc=%2Fazure%2Fdevops%2Fmarketplace-extensibility%2Ftoc.json&amp;view=azure-devops">Azure Managed Identity</a>.</p>
<p>A <a href="https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview">Managed Identity</a> is created as a resource in Azure. Like a Service Principle, a Managed Identity can be granted a license and permissions in Azure DevOps.</p>
<p>To login with a Managed Identity on an Azure hosted VM you can use the command</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">az</span> <span class="n">login</span> <span class="p">-</span><span class="n">-identity</span>
</span></span></code></pre></div><p>This works because the Managed Identity must be associated with the Azure VM you are running the az command on.</p>
<p>The need to be on an Azure resource is the limitation of Managed Identities. So are not appropriate to your needs if you wish to run your script, that uses the Azure CLI, from locations outside Azure.</p>
<h1 id="so-what-do-i-use">So what do I use?</h1>
<p>My most common use-case is to run a script that uses the AZ CLI from within an Azure DevOps Pipeline. This requirement gives need to two ways of working.</p>
<h2 id="when-i-need-to-access-an-azure-subscription">When I need to access an Azure Subscription</h2>
<p>If my script needs permissions beyond Azure DevOps, interacting with a range of Azure resource in a subscription. I could create a Service Principle and grant it permissions to the required resources in Azure, and then pass the Service Principle values into the script as parameters from secret Azure DevOps Pipeline variables.</p>
<p>However, that is a lot a work, and there is a better option built into Azure DevOps, <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops">Service Connections</a>. When you add an &lsquo;Azure Resource Manager&rsquo; service connection you are able to pick from a variety of options such as App Registration (Service Principle), or Managed Identity. Most of the options availble automatically create the required Azure resources required and can support <a href="https://devblogs.microsoft.com/devops/public-preview-of-workload-identity-federation-for-azure-pipelines/">workload identity federation</a> so you don&rsquo;t have to worry over expiring secrets.</p>
<p>You can then very easily run your scripts on a pre authenticated PowerShell session using the <code>AzurePowerShell</code> task</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">AzurePowerShell@5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Run a Script against Azure Resources</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">azureSubscription</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;MyServiceConnection&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">azurePowerShellVersion</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;LatestVersion&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ScriptType</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;InlineScript&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Inline</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # your az commands, not need to login as it is already done
</span></span></span><span class="line"><span class="cl"><span class="sd">      az ....</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">FailOnStandardError</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pwsh</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h2 id="when-i-only-need-to-access-azure-devops">When I only need to access Azure DevOps</h2>
<p>However, my scripts commonly only need to interact with Azure DevOps, and in this case I just use the Build Agent token as a PAT. This is because it avoids the need to set anything up, you can easily pass the token into the script as an environment variable.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nt">trigger</span><span class="p">:</span><span class="w"> </span><span class="l">none</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">PowerShell@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Increment PBI count&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetType</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;inline&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pwsh</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="l">|   </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># I don&#39;t need to call az login as it done automaticallly</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="l">az devops ....</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">AZURE_DEVOPS_EXT_PAT</span><span class="p">:</span><span class="w"> </span><span class="l">$(System.AccessToken)</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Passing Azure DevOps WI field names in PowerShell to Az DevOps CLI as variables</title>
      <link>https://blog.richardfennell.net/posts/passing-ado-wi-fieldnames-in-powershell-to-azcli/</link>
      <pubDate>Mon, 27 Jan 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/passing-ado-wi-fieldnames-in-powershell-to-azcli/</guid>
      <description>&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;The Azure DevOps CLI command &lt;a href=&#34;https://learn.microsoft.com/en-us/cli/azure/boards/work-item?view=azure-cli-latest#az-boards-work-item-update&#34;&gt;az boards work-item update&lt;/a&gt; can take a list of fields as a set if value pairs e.g.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-ps&#34; data-lang=&#34;ps&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;az&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;boards&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;work-item&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;update&lt;/span&gt;  &lt;span class=&#34;nf&#34;&gt;--id&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;123&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;--fields&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Microsoft.VSTS.Scheduling.Effort=10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, if you try to replace the field name with a variable in PowerShell like this&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-ps&#34; data-lang=&#34;ps&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;$fieldname&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;&amp;#34;Microsoft.VSTS.Scheduling.Effort&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;az&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;boards&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;work-item&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;update&lt;/span&gt;  &lt;span class=&#34;nf&#34;&gt;--id&lt;/span&gt; &lt;span class=&#34;mf&#34;&gt;123&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;--fields&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;$fieldname=10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;you will get an error like this&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &amp;ndash;fields argument should consist of space separated &amp;ldquo;field=value&amp;rdquo; pairs.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id=&#34;the-solution&#34;&gt;The Solution&lt;/h2&gt;
&lt;p&gt;The solution is simple, and the same one required if you wish to pass multiple fields into the command, you need to wrapper the set of key value pairs for the fields in quotes e.g.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-issue">The Issue</h2>
<p>The Azure DevOps CLI command <a href="https://learn.microsoft.com/en-us/cli/azure/boards/work-item?view=azure-cli-latest#az-boards-work-item-update">az boards work-item update</a> can take a list of fields as a set if value pairs e.g.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ps" data-lang="ps"><span class="line"><span class="cl"><span class="nf">az</span> <span class="nf">boards</span> <span class="nf">work-item</span> <span class="nf">update</span>  <span class="nf">--id</span> <span class="mf">123</span> <span class="nf">--fields</span> <span class="nf">Microsoft.VSTS.Scheduling.Effort=10</span>
</span></span></code></pre></div><p>However, if you try to replace the field name with a variable in PowerShell like this</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ps" data-lang="ps"><span class="line"><span class="cl"><span class="nf">$fieldname</span> <span class="nf">=</span> <span class="nf">&#34;Microsoft.VSTS.Scheduling.Effort&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nf">az</span> <span class="nf">boards</span> <span class="nf">work-item</span> <span class="nf">update</span>  <span class="nf">--id</span> <span class="mf">123</span> <span class="nf">--fields</span> <span class="nf">$fieldname=10</span>
</span></span></code></pre></div><p>you will get an error like this</p>
<blockquote>
<p>The &ndash;fields argument should consist of space separated &ldquo;field=value&rdquo; pairs.</p></blockquote>
<h2 id="the-solution">The Solution</h2>
<p>The solution is simple, and the same one required if you wish to pass multiple fields into the command, you need to wrapper the set of key value pairs for the fields in quotes e.g.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ps" data-lang="ps"><span class="line"><span class="cl"><span class="nf">$fieldname</span> <span class="nf">=</span> <span class="nf">&#34;Microsoft.VSTS.Scheduling.Effort&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nf">az</span> <span class="nf">boards</span> <span class="nf">work-item</span> <span class="nf">update</span>  <span class="nf">--id</span> <span class="mf">123</span> <span class="nf">--fields</span> <span class="nf">&#34;$fieldname=10&#34;</span>
</span></span></code></pre></div><p>Once this is done the command will work as expected.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Signing files in GitHub Actions</title>
      <link>https://blog.richardfennell.net/posts/signing-files-in-github-actions/</link>
      <pubDate>Mon, 20 Jan 2025 00:00:01 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/signing-files-in-github-actions/</guid>
      <description>&lt;h1 id=&#34;background&#34;&gt;Background&lt;/h1&gt;
&lt;p&gt;I recently &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/why-cant-i-digitally-sign-files-in-my-pipeline/&#34;&gt;wrote about the changes&lt;/a&gt; I had had to make to our Azure DevOps pipelines to address the changes required when code signing with a new DigiCert certificate due to new &lt;a href=&#34;https://knowledge.digicert.com/general-information/new-private-key-storage-requirement-for-standard-code-signing-certificates-november-2022&#34;&gt;private key storage requirements for Code Signing certificates&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Today I had to do the same for a GitHub Actions pipeline. The process is very similar, but there are a few differences in the syntax and the way the secrets are stored.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="background">Background</h1>
<p>I recently <a href="https://blogs.blackmarble.co.uk/rfennell/why-cant-i-digitally-sign-files-in-my-pipeline/">wrote about the changes</a> I had had to make to our Azure DevOps pipelines to address the changes required when code signing with a new DigiCert certificate due to new <a href="https://knowledge.digicert.com/general-information/new-private-key-storage-requirement-for-standard-code-signing-certificates-november-2022">private key storage requirements for Code Signing certificates</a></p>
<p>Today I had to do the same for a GitHub Actions pipeline. The process is very similar, but there are a few differences in the syntax and the way the secrets are stored.</p>
<h1 id="the-solution">The Solution</h1>
<h2 id="step-1-create-a-composite-action">Step 1: Create a Composite Action</h2>
<p>I stored theses steps as a <a href="https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action">Composite Action</a> for easier reuse, but you could put them within a workflow if you prefer.</p>
<p>The composite action installs the DigiCert tools in the first step, and then finds and signs the files in the second.</p>
<blockquote>
<p>Yes, I know I could just have a single step, but I wanted to follow the Azure DevOps flow as closely as possible.</p></blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Sign Code with DigiCert&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Signs the contents of a folder with DigiCert&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">digicert-api-key</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;The DigiCert API key&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tools-download-url</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;The URL for the DigiCert Windows MSI&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="l">https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">signer_p12_file</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;The DigiCert Signer P12 File as base64 encoded string&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cert_crt_file</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;The DigiCert Certificate CRT File as base64 encoded string&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">digicert_host</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;The DigiCert Host&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="l">https://clientauth.one.digicert.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">keypair_alias</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;DigiCert certifiate alias&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">password</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Digicert password&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">file-path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Path to scan for files&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">runs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">using</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;composite&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install DigiCert Client Tools</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">pwsh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">| </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="l">curl -X GET  ${{ inputs.tools-download-url }} -H &#34;x-api-key:${{ inputs.digicert-api-key }} &#34; -o smtools-windows-x64.msi </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="l">msiexec /i smtools-windows-x64.msi /quiet /qn </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Code Sign Files</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">pwsh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Define the base path where signtool.exe is located
</span></span></span><span class="line"><span class="cl"><span class="sd">      $basePath = &#34;C:\Program Files (x86)\Windows Kits\10\bin&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Filtering via version and architecture, could use just one of the these, depends on needs
</span></span></span><span class="line"><span class="cl"><span class="sd">      $preferredVersion = &#34;x64&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Get the matching signtool path (pick the last in the list if multiple returned)
</span></span></span><span class="line"><span class="cl"><span class="sd">      $signtoolPath = (Get-ChildItem -Path $basePath -Recurse -Filter &#34;signtool.exe&#34; -File | Where-Object { $_.FullName -like &#34;*\$preferredVersion\*&#34; })[-1] | Select-Object -ExpandProperty FullName
</span></span></span><span class="line"><span class="cl"><span class="sd">      write-host &#34;Found signtool at $signtoolPath&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      
</span></span></span><span class="line"><span class="cl"><span class="sd">      set-content -Path &#39;signer.p12.base64&#39; -Value &#39;${{ inputs.signer_p12_file }}&#39; 
</span></span></span><span class="line"><span class="cl"><span class="sd">      certutil -decode -f &#39;signer.p12.base64&#39; &#39;DigiCert Signer Certificate_pkcs12.p12&#39;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      set-content -Path &#39;cert.crt.base64&#39; -Value &#39;${{ inputs.cert_crt_file }}&#39; 
</span></span></span><span class="line"><span class="cl"><span class="sd">      certutil -decode -f &#39;cert.crt.base64&#39; &#39;cert.crt&#39;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Find the files that match 
</span></span></span><span class="line"><span class="cl"><span class="sd">      write-host &#34;Finding files that match&#39;the path ./src/sample/bin/**/**/*.dll&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      Get-ChildItem -Path &#34;${{ inputs.file-path }}&#34; | ForEach-Object {
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filePath = $_.FullName
</span></span></span><span class="line"><span class="cl"><span class="sd">        write-host &#34;Signing file $filePath&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        &amp; $signtoolPath sign /v /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 /csp &#34;DigiCert Signing Manager KSP&#34; /kc &#34;${{ inputs.keypair_alias}}&#34; /f &#34;cert.crt&#34; $filePath
</span></span></span><span class="line"><span class="cl"><span class="sd">      }   </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SM_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">${{ inputs.digicert_host }} </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SM_API_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${{ inputs.digicert-api-key }} </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SM_CLIENT_CERT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${{ inputs.password }} </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SM_CLIENT_CERT_FILE</span><span class="p">:</span><span class="w"> </span><span class="l">DigiCert Signer Certificate_pkcs12.p12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SM_TLS_SKIP_VERIFY</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h2 id="step-2-store-the-certificates-as-secrets">Step 2: Store the Certificates as Secrets</h2>
<p>As with Azure DevOps implementation we now have a pair of files, the .CRT certificate and the .P12 signer’s certificate.</p>
<p>GitHub does not have a feature like <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops">Azure DevOps Secure Files</a>. So, we have to store the certificates as secrets.</p>
<p>To store these certificates as GitHub Secrets you need to encode the file content into a base64 string and then add it as a secret in your GitHub repository, or Organisation or Enterprise. Use whichever level works best for you, in my case I chose to store them as Organisation secrets.</p>
<p>Here are the steps:</p>
<ol>
<li>Open Command Prompt or PowerShell.</li>
<li>Use the <code>certutil</code> command to encode the file:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">certutil -encode yourfile.crt yourfile.crt.base64
</span></span></code></pre></div></li>
<li>Open the generated <code>yourfile.crt.base64</code> file in a text editor (e.g., Notepad) and copy its content.</li>
<li>Add the secret to GitHub at the level you chose:
<ul>
<li>Name your secret (e.g. CRT_FILE).</li>
<li>Paste the base64-encoded content into the Value field.</li>
</ul>
</li>
</ol>
<p>Repeat this process for both the .CRT and the .P12 file.</p>
<h2 id="step-3-store-the-other-digicert-settings-as-secrets-and-variables">Step 3: Store the other DigiCert settings as secrets and variables</h2>
<p>The other DigiCert settings can be stored as a mixture of secrets and variables. Use variables if reading the value in the log is not deemed a security risk.</p>
<p>The secrets:</p>
<ul>
<li>DIGICERT_API_KEY</li>
<li>DIGICERT_SIGNER_P12_FILE as base64 encoded string (see above)</li>
<li>DIGICERT_CERT_CRT_FILE as base64 encoded string (see above)</li>
<li>DIGICERT_CLIENT_CERT_PASSWORD</li>
</ul>
<p>and as a variable:</p>
<ul>
<li>DIGICERT_KEYPAIR_ALIAS</li>
</ul>
<h2 id="step-4-using-the-composite-action-in-a-workflow">Step 4: Using the Composite Action in a Workflow</h2>
<p>Finally pull it all together in your workflow</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Sign files with Digicert</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">blackmarble/Sign-Code-With-DigiCert@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">digicert-api-key</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{ secrets.DIGICERT_API_KEY }}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">signer_p12_file</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{ secrets.DIGICERT_SIGNER_P12_FILE }}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">cert_crt_file</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{ secrets.DIGICERT_CERT_CRT_FILE }}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">keypair_alias</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{ vars.DIGICERT_KEYPAIR_ALIAS}}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{ secrets.DIGICERT_CLIENT_CERT_PASSWORD }}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">file-path</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;./src/Sample/bin/**/**/*.dll&#39;</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Re-authenticating Microsoft Authenticator after swapping your phone</title>
      <link>https://blog.richardfennell.net/posts/re-authenticating-mfa-after-swapping-your-phone/</link>
      <pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/re-authenticating-mfa-after-swapping-your-phone/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This one of those posts that is more a note to self as I keep forgetting how to do this, but I hope it helps others.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h1 id=&#34;background&#34;&gt;Background&lt;/h1&gt;
&lt;p&gt;I use &lt;a href=&#34;https://www.microsoft.com/en-gb/security/mobile-authenticator-app&#34;&gt;Microsoft&amp;rsquo;s Authenticator&lt;/a&gt; to provide MFA on a number of accounts. I recently swapped my Android phone and had to, after restoring a backup, re-authenticate some accounts on the new device.&lt;/p&gt;
&lt;p&gt;This was a simple process for most accounts, just a case of validating the code generated by the new device, but I had a problem with the entries where my Black Marble Entra ID account was &lt;a href=&#34;https://learn.microsoft.com/en-us/entra/external-id/b2b-quickstart-add-guest-users-portal&#34;&gt;a guest&lt;/a&gt; in other company&amp;rsquo;s Entra ID directories.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>This one of those posts that is more a note to self as I keep forgetting how to do this, but I hope it helps others.</p></blockquote>
<h1 id="background">Background</h1>
<p>I use <a href="https://www.microsoft.com/en-gb/security/mobile-authenticator-app">Microsoft&rsquo;s Authenticator</a> to provide MFA on a number of accounts. I recently swapped my Android phone and had to, after restoring a backup, re-authenticate some accounts on the new device.</p>
<p>This was a simple process for most accounts, just a case of validating the code generated by the new device, but I had a problem with the entries where my Black Marble Entra ID account was <a href="https://learn.microsoft.com/en-us/entra/external-id/b2b-quickstart-add-guest-users-portal">a guest</a> in other company&rsquo;s Entra ID directories.</p>
<p>I could not remember the process.</p>
<h1 id="the-solution">The Solution</h1>
<p>When the Authenticator app shows the message</p>
<blockquote>
<p>Action Required</p>
<p>Scan the QR code provided by your organization to finish recovering your account</p></blockquote>
<p>and the account is in the form <code>richard_domain.com#EXT#@anothercompany.onmicrosoft.com</code></p>
<p>The steps are as follows (assuming you still have access to the old MFA device):</p>
<ol>
<li>In a browser open <a href="https://mysignins.microsoft.com/security-info">https://mysignins.microsoft.com/security-info</a></li>
<li>Sign in with your own company Entra ID account i.e. in this example <code>richard@domain.com</code></li>
<li>Click the &lsquo;organisation&rsquo; button (a icon of a directory tree, in the top right near the help button).
<img alt="Organisation Button" loading="lazy" src="/images/rfennell/mfa-password-reset-1.png"></li>
<li>A list of organisations you are a member of will appear as a panel on the right of the page
<img alt="Organisation List" loading="lazy" src="/images/rfennell/mfa-password-reset-2.png"></li>
<li>Pick the required organisation from the menu on the right, in my <code>richard_domain.com#EXT#@anothercompany.onmicrosoft.com</code> example, this would be an organisation called <code>anothercompany</code></li>
<li>You will need to authenticate with the old MFA device</li>
<li>On the refreshed <a href="https://mysignins.microsoft.com/security-info">https://mysignins.microsoft.com/security-info</a> page for the selected organisation you can now use the <code>add a new device process</code> to generate the QR code</li>
<li>On the new MFA device pick the account to be re-authenticate and scan the QR code. This is all that is required to add the new device as sign-in method (note you do not have to completed the new device wizard in the browser, but it is probably a good idea as a final check.).</li>
</ol>
<blockquote>
<p>If you don&rsquo;t have access to the old MFA device, you will need to contact the organisation&rsquo;s IT support to get them to reset the MFA for you.</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Why am I getting no private key is available error when I try to digitally sign files in my Azure DevOps Pipeline?</title>
      <link>https://blog.richardfennell.net/posts/why-cant-i-digitally-sign-files-in-my-pipeline/</link>
      <pubDate>Wed, 11 Dec 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-cant-i-digitally-sign-files-in-my-pipeline/</guid>
      <description>&lt;h1 id=&#34;background&#34;&gt;Background&lt;/h1&gt;
&lt;p&gt;It is becoming increasingly important to sign files digitally to ensure that they have not been tampered with, to secure the software supply chain. This is something we have done for a good while as a step in our Azure DevOps pipelines. However, recent(ish) changes in the way certificates are issued has meant we have had to revise our approach.&lt;/p&gt;
&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;We used to use a .PFX file, stored as an Azure DevOps secure file, that contained the public and private keys and was accessed using a password, to sign our files.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="background">Background</h1>
<p>It is becoming increasingly important to sign files digitally to ensure that they have not been tampered with, to secure the software supply chain. This is something we have done for a good while as a step in our Azure DevOps pipelines. However, recent(ish) changes in the way certificates are issued has meant we have had to revise our approach.</p>
<h1 id="the-problem">The Problem</h1>
<p>We used to use a .PFX file, stored as an Azure DevOps secure file, that contained the public and private keys and was accessed using a password, to sign our files.</p>
<p>However, when we renewed our code signing certificate with DigiCert we found this approach no longer valid due to <a href="https://knowledge.digicert.com/general-information/new-private-key-storage-requirement-for-standard-code-signing-certificates-november-2022">new private key storage requirements for Code Signing certificates</a>.</p>
<p>The basic issue is that now when we sign a file, the signing tool needs to make a call back to a secure location to validate the certificate. In our case Digicert&rsquo;s Keylocker service.</p>
<h1 id="the-solution">The Solution</h1>
<p>This change required some changes to our pipelines.</p>
<ol>
<li>Store our .CRT certificate file as a secure file in Azure DevOps</li>
<li>Store our .p12 signer&rsquo;s certificate file as a secure file in Azure DevOps</li>
<li>Store our DigiCert account settings as Azure DevOps pipeline variables (a mixture of standard and secret ones)</li>
<li>Update our pipeline as follows</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">SSMClientToolsSetup@1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Install DigiCert Client Tools </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">DownloadSecureFile@1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Download DigiCert Code Signing Certificate File</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secureFile</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;DigicertCodeSigningCert.crt&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">DownloadSecureFile@1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Download DigiCert Signer Certificate File</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secureFile</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;DigiCertSignerCertificate.p12&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">PowerShell@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Code Sign Files</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetType</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;inline&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Define the base path where signtool.exe is located
</span></span></span><span class="line"><span class="cl"><span class="sd">      $basePath = &#34;C:\Program Files (x86)\Windows Kits\10\bin&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Filtering via version and architecture, could use just one of the these, depends on needs
</span></span></span><span class="line"><span class="cl"><span class="sd">      $preferredVersion = &#34;x64&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Get the matching signtool path (pick the last in the list if multiple returned)
</span></span></span><span class="line"><span class="cl"><span class="sd">      $signtoolPath = (Get-ChildItem -Path $basePath -Recurse -Filter &#34;signtool.exe&#34; -File | Where-Object { $_.FullName -like &#34;*\$preferredVersion\*&#34; })[-1] | Select-Object -ExpandProperty FullName
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      # Find the files that match filter that need to be signed
</span></span></span><span class="line"><span class="cl"><span class="sd">      write-host &#34;Finding files to sign&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">      Get-ChildItem -Path &#34;$(Build.SourcesDirectory)/myproject/**/*.exe&#34; | ForEach-Object {
</span></span></span><span class="line"><span class="cl"><span class="sd">         $filePath = $_.FullName
</span></span></span><span class="line"><span class="cl"><span class="sd">         write-host &#34;Signing file $filePath&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">         &amp; $signtoolPath sign /v /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 /csp &#34;DigiCert Signing Manager KSP&#34; /kc &#34;$(SM_KEYPAIR_ALIAS)&#34; /f &#34;$(Agent.TempDirectory)\DigicertCodeSigningCert.crt&#34; $filePath
</span></span></span><span class="line"><span class="cl"><span class="sd">       }   </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">SM_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">$(SM_HOST) </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">SM_API_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">$(SM_API_KEY) </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">SM_CLIENT_CERT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">$(SM_CLIENT_CERT_PASSWORD) </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">SM_CLIENT_CERT_FILE</span><span class="p">:</span><span class="w"> </span><span class="l">$(Agent.TempDirectory)\DigiCert Signer Certificate_pkcs12.p12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">SM_TLS_SKIP_VERIFY</span><span class="p">:</span><span class="w"> </span><span class="l">$(SM_TLS_SKIP_VERIFY)</span><span class="w">
</span></span></span></code></pre></div><h1 id="the-gotcha-with-variables">The gotcha with variables</h1>
<p>I have blogged a number of times before about the need to <a href="https://blogs.blackmarble.co.uk/rfennell/getting-confused-over-azure-devops-pipeline-variable-evaluation/">be careful with the syntax for Azure DevOps variables</a>. Guess what, I got caught out by this again!</p>
<p>I had initially used the <code>${{ variables.XXX }}</code> format for injecting the Azure DevOps variables as PowerShell environment variables e.g.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">SM_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">${{ variables.SM_HOST }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">SM_API_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${{ variables.SM_API_KEY }}</span><span class="w">
</span></span></span></code></pre></div><p>This worked fine for the standard variables, but not for the secret ones. The secret ones were not being injected into the script as environment variables so when we tried to sign a file we got the error</p>
<blockquote>
<p>SignTool Error: No private key is available.</p></blockquote>
<p>This was a case of trying to be too clever with the syntax, the correct syntax in this case is to use the standard macro syntax <code>$(XXX)</code> format for all variables, especially the secret ones.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">SM_HOST</span><span class="p">:</span><span class="w"> </span><span class="l">$(SM_HOST)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">SM_API_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">$(SM_API_KEY)</span><span class="w">
</span></span></span></code></pre></div><p>Once this was done, the signing process worked as expected.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Inject a step into Web Deploy</title>
      <link>https://blog.richardfennell.net/posts/inject-a-step-into-msdeploy/</link>
      <pubDate>Wed, 04 Dec 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/inject-a-step-into-msdeploy/</guid>
      <description>&lt;p&gt;I really like &lt;a href=&#34;https://www.iis.net/downloads/microsoft/web-deploy&#34;&gt;Web Deploy&lt;/a&gt;, it is a powerful tool for &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/porting-my-visual-studio-parameters-xml-generator-tool-to-visual-studio-2022-preview/?query=parameters.&#34;&gt;injecting parameters&lt;/a&gt; whilst deploying web applications to both Azure or an on-premise IIS Server.&lt;/p&gt;
&lt;p&gt;Every project is different, and sometimes you need to be able to inject a step into the Web Deploy package creation process to complete some extra step. This can be done by adding a target to the &lt;code&gt;.csproj&lt;/code&gt; project file.&lt;/p&gt;
&lt;p&gt;The following example shows how you could sign the assemblies before the Web Deploy package is created.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I really like <a href="https://www.iis.net/downloads/microsoft/web-deploy">Web Deploy</a>, it is a powerful tool for <a href="https://blogs.blackmarble.co.uk/rfennell/porting-my-visual-studio-parameters-xml-generator-tool-to-visual-studio-2022-preview/?query=parameters.">injecting parameters</a> whilst deploying web applications to both Azure or an on-premise IIS Server.</p>
<p>Every project is different, and sometimes you need to be able to inject a step into the Web Deploy package creation process to complete some extra step. This can be done by adding a target to the <code>.csproj</code> project file.</p>
<p>The following example shows how you could sign the assemblies before the Web Deploy package is created.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"> <span class="nt">&lt;Target</span> <span class="na">Name=</span><span class="s">&#34;SignWebExe&#34;</span> <span class="na">AfterTargets=</span><span class="s">&#34;GenerateMsdeployManifestFiles&#34;</span> <span class="na">BeforeTargets=</span><span class="s">&#34;PackageUsingManifest&#34;</span> <span class="na">Condition=</span><span class="s">&#34;&#39;$(Configuration)&#39; == &#39;Release&#39;&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;PropertyGroup&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&lt;Cmd&gt;</span>signtool.exe sign /debug /f &#34;$(certPath)&#34; /p &#34;$(certPassword)&#34; &#34;$(ProjectDir)obj\$(Configuration)\Package\PackageTmp\bin\MyNamespace*.dll&#34;<span class="nt">&lt;/Cmd&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/PropertyGroup&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;Message</span> <span class="na">Text=</span><span class="s">&#34;Signing web deploy executable with command: $(Cmd)&#34;</span> <span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;Exec</span> <span class="na">Command=</span><span class="s">&#34;$(Cmd)&#34;</span> <span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/Target&gt;</span>
</span></span></code></pre></div><p>The required parameters for this extra step are then passed in as MSBuild arguments. For example, in an Azure DevOps pipeline, you could use the following task:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">VSBuild@1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Build Core Services Solution&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">solution</span><span class="p">:</span><span class="w"> </span><span class="l">src/MySolution.sln</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">msbuildArgs</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;/p:DeployOnBuild=true;PublishProfile=Release /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DeployIisAppPath=&#34;__SITENAME__&#34; /p:certPassword=&#34;$(SigningPassword)&#34; /p:certPath=&#34;$(SigningCertFilePath)&#34;&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">platform</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$(BuildPlatform)&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">configuration</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$(BuildConfiguration)&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">clean</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>ProjectFileIndexer exceptions in SonarQube</title>
      <link>https://blog.richardfennell.net/posts/projectfileindexer-exceptions-in-sonarqube/</link>
      <pubDate>Thu, 31 Oct 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/projectfileindexer-exceptions-in-sonarqube/</guid>
      <description>&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;We are running our &lt;a href=&#34;https://www.sonarsource.com/products/sonarqube/&#34;&gt;SonarQube&lt;/a&gt; instance as an &lt;a href=&#34;https://devblogs.microsoft.com/premier-developer/sonarqube-hosted-on-azure-app-service/&#34;&gt;Azure hosted Docker container&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Over the past few weeks we have been seeing intermittent occurrences of the &lt;code&gt;ProjectFileIndexer&lt;/code&gt; exception during the SonarQube analysis step in our Azure DevOps pipelines.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;##[error]java.lang.IllegalStateException: Unable to load component class org.sonar.scanner.scan.filesystem.ProjectFileIndexer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I looked closer at the exception stack, I could see at the bottom there was always a timeout error when trying to access the &lt;code&gt;project.protobuf&lt;/code&gt; file from SonarQube.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-issue">The Issue</h2>
<p>We are running our <a href="https://www.sonarsource.com/products/sonarqube/">SonarQube</a> instance as an <a href="https://devblogs.microsoft.com/premier-developer/sonarqube-hosted-on-azure-app-service/">Azure hosted Docker container</a>.</p>
<p>Over the past few weeks we have been seeing intermittent occurrences of the <code>ProjectFileIndexer</code> exception during the SonarQube analysis step in our Azure DevOps pipelines.</p>
<pre tabindex="0"><code>##[error]java.lang.IllegalStateException: Unable to load component class org.sonar.scanner.scan.filesystem.ProjectFileIndexer
</code></pre><p>When I looked closer at the exception stack, I could see at the bottom there was always a timeout error when trying to access the <code>project.protobuf</code> file from SonarQube.</p>
<pre tabindex="0"><code>Caused by: java.lang.IllegalStateException: Fail to request url: https://sonarqube.mydomain.co.uk/batch/project.protobuf?key=mykey
</code></pre><p>If I tried to open the URL in a browser, authenticated with SonarQube, most of the time the expected <code>project.protobuf</code> file was returned, but sometimes it was not.</p>
<p>Also, at random times, the SonarQube UI would take many seconds to refresh when I selected a project.</p>
<h2 id="the-solution">The Solution</h2>
<p>In the past I have usually found any issues with SonarQube are <a href="https://blogs.blackmarble.co.uk/rfennell/its-the-sonarqube-indexes-again/">ElasticSearch related</a>, but not so this time.</p>
<p>The issue turned out to be a lack of SQL resources.</p>
<p>Our SonarQube instance was running on</p>
<ul>
<li>Azure Web App (Premium V3 P1V3 2vCPU &amp; 8Gb)</li>
<li>Azure SQL DB (Standard S2 50 DTU)</li>
<li>Logs/Config/ES indexes etc/ on Azure Standard General Purpose V2) File storage</li>
</ul>
<p>When I look at the <a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/query-performance-insight-use?view=azuresql">Azure SQL Query Performance Insights</a>, I could see that the SQL DTU resources were maxing out at 100% for long periods of time, this occurred especially when the SonarQube analysis was restarted.</p>
<p>I increased the SQL resource to a Standard S3 100 DTU, the scaling took 15 minutes or so. Once this was done, the issue seemed to be resolved, though it is always hard to be sure with intermittent issues.</p>
<p>My assumption is that there are some SQL queries that are slow to run and are potentially blocking to other queries. By increasing the SQL resources, the these queries could be processed more quickly, and the blocking reduced to a level where it was not an issue.</p>
<p>You can see the impact of the change in the SQL DTU usage graph below. After the increase in SQL resources, we are not maxing out the server DTUs, so the SonarQube analysis can running without issues.</p>
<p><img alt="SQL DTU usage graph" loading="lazy" src="/images/rfennell/sonarpref1.png"></p>
<p>I think the key take away is to make sure you have enough SQL resources, don&rsquo;t max out your assigned DTUs, as this will cause issues.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why is my SnipeIT instance suddenly slow?</title>
      <link>https://blog.richardfennell.net/posts/why-is-my-snipeit-instance-suddenly-slow/</link>
      <pubDate>Mon, 28 Oct 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-is-my-snipeit-instance-suddenly-slow/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;As I have &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/setting-up-snipe-it-on-azure/&#34;&gt;blogged previously&lt;/a&gt;, we run a SnipeIT instance to manage our IT assets, hosted in Azure using Docker.&lt;/p&gt;
&lt;p&gt;This has been working well for us for the past year, but recently we have noticed that the system has become very slow to respond.&lt;/p&gt;
&lt;p&gt;Looking on the Azure portal, we can see that around the 15th of October the web app&amp;rsquo;s response times have gone from milliseconds to 10s of seconds&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>As I have <a href="https://blogs.blackmarble.co.uk/rfennell/setting-up-snipe-it-on-azure/">blogged previously</a>, we run a SnipeIT instance to manage our IT assets, hosted in Azure using Docker.</p>
<p>This has been working well for us for the past year, but recently we have noticed that the system has become very slow to respond.</p>
<p>Looking on the Azure portal, we can see that around the 15th of October the web app&rsquo;s response times have gone from milliseconds to 10s of seconds</p>
<p><img alt="Slow web response times" loading="lazy" src="/images/rfennell/snipe-slow1.png"></p>
<h2 id="investigation">Investigation</h2>
<p>The first question we asked, as you always should, was what had changed?</p>
<p>The answer was nothing obvious.</p>
<ul>
<li>We had not changed the Azure Web App or MySQL SKUs</li>
<li>It is true that we had not updated the SnipeIT version since July, but we were on the current major and minor version and only a few patch versions behind.</li>
<li>We had not added any significant new assets or users in the last few months.</li>
</ul>
<p>So it all appeared strange what had changed?</p>
<p>We of course tried restarting the container instance, and cleaning down logfiles. We have seen slow performance in other systems when the logfiles get too large.</p>
<p>We also tried upping the SKU of the Azure Web App, but none of this made a difference.</p>
<p>We then set the SnipeIT log level to debug, this showed the high response times were not due to the web app being slow to respond, but the time taken to get data from the MySQL instance.</p>
<p>Looking again at the metrics in the Azure portal, I noticed that the MySQL instance started showing a higher CPU usage and increased DB connections around the same time as the web app started to slow down.</p>
<p><img alt="MySQL CPU" loading="lazy" src="/images/rfennell/snipe-slow2.png"></p>
<p>These new CPU and connection levels did not seem excessive, but they were higher than they had been previously. I then noticed in the Azure Portal a warning message, that we had used up all the <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/b-series-cpu-credit-model/b-series-cpu-credit-model#b-series-cpu-credit-model">MySQL Burstable SKU credits</a>.</p>
<p><img alt="MySQL warning" loading="lazy" src="/images/rfennell/snipe-slow3.png"></p>
<h2 id="solution">Solution</h2>
<p>The solution was in fact simple, to move the MySQL instance to a higher SKU, from <code>Standard_B1s</code> to <code>Standard_B2ms</code>, increasing the running cost by only a few pence a month, from £2.17 to £2.25 PCM</p>
<p>This SKU change doubles the resources available to MySQL, but more importantly allows us to build up more <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/b-series-cpu-credit-model/b-series-cpu-credit-model#b-series-cpu-credit-model">burstable CPU credit</a> for the times we need it in a month.</p>
<p>So now I have a working solution, it as all down to the fact that we had been running close to the limit of our MySQL SKU for a while, and it was a matter of time before the issue occurred.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using Azure Service Connection names that are stored in variables group in Azure DevOps Pipeline</title>
      <link>https://blog.richardfennell.net/posts/using-azure-service-connection-names-that-are-stored-in-variables-group-ado-pipeline/</link>
      <pubDate>Mon, 21 Oct 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-azure-service-connection-names-that-are-stored-in-variables-group-ado-pipeline/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;If you are using staged deployment in Azure DevOps, you will probably have multiple Azure Service Connections. So, it makes sense that you might want to use a Service Connection name that is stored in a variable group as a parameter to a templated YAML pipeline.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# the build pipeline&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;stages&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;stage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;UAT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;deployment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ARM_Provisioning&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;timeoutInMinutes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;environment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Staging&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;variables&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;group&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;UAT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;pool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;vmImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;windows-latest&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;strategy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runOnce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;deploy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;template&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;YAMLTemplates\ProvisionUsingARM.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;              &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;parameters&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;AzureResourceGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;$(AzureResourceGroup)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;AzureServiceConnection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;$(AzureServiceConnection)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;stage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;PROD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;jobs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;deployment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;ARM_Provisioning&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;timeoutInMinutes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;environment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Staging&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;variables&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;group&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;PROD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;pool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;vmImage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;windows-latest&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;strategy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;runOnce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;deploy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;steps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;template&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;YAMLTemplates\ProvisionUsingARM.yml&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;              &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;parameters&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;AzureResourceGroup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;$(AzureResourceGroup)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;AzureServiceConnection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;$(AzureServiceConnection)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With a template &lt;code&gt;YAMLTemplates\ProvisionUsingARM.yml&lt;/code&gt; that uses the &lt;code&gt;AzureServiceConnection&lt;/code&gt; variable&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>If you are using staged deployment in Azure DevOps, you will probably have multiple Azure Service Connections. So, it makes sense that you might want to use a Service Connection name that is stored in a variable group as a parameter to a templated YAML pipeline.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># the build pipeline</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">deployment</span><span class="p">:</span><span class="w"> </span><span class="l">ARM_Provisioning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeoutInMinutes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;windows-latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">runOnce</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">YAMLTemplates\ProvisionUsingARM.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureResourceGroup</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureResourceGroup)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureServiceConnection)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">deployment</span><span class="p">:</span><span class="w"> </span><span class="l">ARM_Provisioning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeoutInMinutes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;windows-latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">runOnce</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">YAMLTemplates\ProvisionUsingARM.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureResourceGroup</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureResourceGroup)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureServiceConnection)</span><span class="w">
</span></span></span></code></pre></div><p>With a template <code>YAMLTemplates\ProvisionUsingARM.yml</code> that uses the <code>AzureServiceConnection</code> variable</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">AzureResourceGroup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">AzureServiceConnection</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">AzureResourceManagerTemplateDeployment@3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName: &#39;Azure Deployment</span><span class="p">:</span><span class="w"> </span><span class="l">Create Or Update Resource Group&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deploymentScope</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Resource Group&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">azureResourceManagerConnection</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{ parameters.AzureServiceConnection}}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Create Or Update Resource Group&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resourceGroupName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;${{parameters.azureResourceGroup}}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="l">...</span><span class="w">
</span></span></span></code></pre></div><h2 id="the-issue">The Issue</h2>
<p>If I declared a pair of variables in the <code>UAT</code> and <code>PROD</code> variable groups with the names <code>AzureServiceConnection</code> and <code>AzureResourceGroup</code> I would expect each stage to pick up the appropriate variable group and use the correct service connection.</p>
<p>However, this is not the case. The pipeline will fail with the following error message when you validate or try to queue the pipeline.</p>
<blockquote>
<p>There was a resource authorization issue: &ldquo;The pipeline is not valid. Job ARM_Provisioning: Step input azureResourceManagerConnection references service connection $(AzureServiceConnection) which could not be found. The service connection does not exist, has been disabled or has not been authorized for use. For authorization details, refer to <a href="https://aka.ms/yamlauthz.%22">https://aka.ms/yamlauthz."</a></p></blockquote>
<p>Basically, the <code>$(AzureServiceConnection)</code> variable is not being expanded during template validation.</p>
<h2 id="analysis">Analysis</h2>
<h3 id="compile-and-runtime-variables">Compile and Runtime variables</h3>
<p>I tried all the <a href="https://blogs.blackmarble.co.uk/rfennell/getting-confused-over-azure-devops-pipeline-variable-evaluation/">options for the variable declaration in the YAML</a></p>
<ul>
<li>Standard Macro format <code>$(AzureResourceGroup)</code></li>
<li>Expression format <code>$[AzureResourceGroup]</code></li>
<li>Runtime expression format <code>${{ variables.AzureResourceGroup}}</code></li>
</ul>
<p>None of these helped.</p>
<h3 id="hardcoding-the-connection-name">Hardcoding the connection name</h3>
<p>If you hardcode the service connection name in the outer YAML pipeline file, the pipeline will work as expected.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># the build pipeline</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">deployment</span><span class="p">:</span><span class="w"> </span><span class="l">ARM_Provisioning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeoutInMinutes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;windows-latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">runOnce</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">YAMLTemplates\ProvisionUsingARM.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureResourceGroup</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureResourceGroup)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">deployment</span><span class="p">:</span><span class="w"> </span><span class="l">ARM_Provisioning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeoutInMinutes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;windows-latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">runOnce</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">YAMLTemplates\ProvisionUsingARM.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureResourceGroup</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureResourceGroup)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span></code></pre></div><p>But that is not at all what we are after, but it did at least show that it was possible to pass in the service connection name as a string parameter.</p>
<h3 id="using-a-global-variable">Using a global variable</h3>
<p>If I declared a global pipeline variable, at the top of the pipeline file, it could be validated and queued without any issues.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span><span class="l">....</span><span class="w">
</span></span></span></code></pre></div><p>But again this was still not what I was after.</p>
<h2 id="the-solution">The Solution</h2>
<p>In my project I have a number of variable groups, one for each stage of deployment. What I found I needed to do was to declare one of them as a global variable for the whole pipeline. This would allow the pipeline to be validated and queued without any issues, but in at each deployment stage this initial value is overridden by the variable group associated with the stage.</p>
<p>This does rely on the fact that the variable groups for each stage contain the same variable names.</p>
<p>The revised YAML looks as follows</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># the build pipeline</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># we have to globally declare this variable group, though it is override at the deployment job level</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># if we don&#39;t do this the $(AzureServiceConnection) variable used by the Azure Resource deployment fails queue time validation </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">deployment</span><span class="p">:</span><span class="w"> </span><span class="l">ARM_Provisioning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeoutInMinutes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">UAT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;windows-latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">runOnce</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">YAMLTemplates\ProvisionUsingARM.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureResourceGroup</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureResourceGroup)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureServiceConnection)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">deployment</span><span class="p">:</span><span class="w"> </span><span class="l">ARM_Provisioning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeoutInMinutes</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Staging&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">variables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">PROD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">vmImage</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;windows-latest&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">runOnce</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">YAMLTemplates\ProvisionUsingARM.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureResourceGroup</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureResourceGroup)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">AzureServiceConnection</span><span class="p">:</span><span class="w"> </span><span class="l">$(AzureServiceConnection)</span><span class="w">
</span></span></span></code></pre></div><p>So I think a valid workaround for another strange YAML variable expansion issue in Azure DevOps.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Editing multiple files in the Azure DevOps UI and committing them in a single commit</title>
      <link>https://blog.richardfennell.net/posts/editing-multiple-files-in-azdo-ui-in-a-single-commit/</link>
      <pubDate>Fri, 18 Oct 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/editing-multiple-files-in-azdo-ui-in-a-single-commit/</guid>
      <description>&lt;p&gt;One of the most useful, and it seems relatively unknown, features in the GitHub web UI is the &lt;a href=&#34;https://github.com/github/dev&#34;&gt;ability to edit multiple files in the UI and commit them in a single commit&lt;/a&gt;. This is done by loading VS Code in the browser when in the code view by pressing &lt;strong&gt;.&lt;/strong&gt; (the full stop)&lt;/p&gt;
&lt;p&gt;The reason I find this so useful is that it allows me to make a series of small related changes to a project without having to clone the repository or using a &lt;a href=&#34;https://github.com/features/codespaces&#34;&gt;CodeSpace&lt;/a&gt;, very useful when editing the related YAML files of reusable workflows in GitHub Actions.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>One of the most useful, and it seems relatively unknown, features in the GitHub web UI is the <a href="https://github.com/github/dev">ability to edit multiple files in the UI and commit them in a single commit</a>. This is done by loading VS Code in the browser when in the code view by pressing <strong>.</strong> (the full stop)</p>
<p>The reason I find this so useful is that it allows me to make a series of small related changes to a project without having to clone the repository or using a <a href="https://github.com/features/codespaces">CodeSpace</a>, very useful when editing the related YAML files of reusable workflows in GitHub Actions.</p>
<p>I was recently working on a templated YAML Azure DevOps Pipeline project and pressed <strong>.</strong> our of habit and was really pleased to find that this <a href="https://code.visualstudio.com/docs/editor/vscode-web">VS Code in the browser feature</a> is also available for Azure DevOps.</p>
<p>So next time you need to make a series of small changes to a project in Azure DevOps why not give it a try.</p>
<p><img alt="Editing multiple files in the Azure DevOps UI and committing them in a single commit" loading="lazy" src="/images/rfennell/vscode-azdo.png"></p>
]]></content:encoded>
    </item>
    <item>
      <title>Generating Visual Studio SQL Database Projects from the command line</title>
      <link>https://blog.richardfennell.net/posts/generating-vs-sql-projects-from-the-command-line/</link>
      <pubDate>Fri, 27 Sep 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/generating-vs-sql-projects-from-the-command-line/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is is one of those posts I write so I remember how to do something in the future.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I recently had a need to generate many Visual Studio SQL Database Projects from existing databases. Being a good &amp;rsquo;lazy developer&amp;rsquo; I wanted to do this from the command line so I could automate the process, but it took me far to long to work out how&lt;/p&gt;
&lt;h2 id=&#34;the-manual-way&#34;&gt;The Manual Way&lt;/h2&gt;
&lt;p&gt;If you only have one database to import you can do this manually by using the &lt;strong&gt;Import&lt;/strong&gt; option in Visual Studio for an individual SQL Database Project.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p><em>This is is one of those posts I write so I remember how to do something in the future.</em></p></blockquote>
<h2 id="background">Background</h2>
<p>I recently had a need to generate many Visual Studio SQL Database Projects from existing databases. Being a good &rsquo;lazy developer&rsquo; I wanted to do this from the command line so I could automate the process, but it took me far to long to work out how</p>
<h2 id="the-manual-way">The Manual Way</h2>
<p>If you only have one database to import you can do this manually by using the <strong>Import</strong> option in Visual Studio for an individual SQL Database Project.</p>
<ol>
<li>Open Visual Studio</li>
<li>Create a new project using the <strong>SQL Server</strong> -&gt; <strong>SQL Server Database Project</strong> template</li>
<li>Right click on the project in the Solution Explorer and select <strong>Import</strong> -&gt; <strong>Database</strong> and follow the wizard.</li>
</ol>
<p><img alt="Import Database" loading="lazy" src="/images/rfennell/ssdtimport.png"></p>
<h2 id="the-command-line-way">The Command Line Way</h2>
<p>For some reason I really struggled to find the command line syntax to perform the same action,  so I am documenting it here for future reference.</p>
<p>The key command is <a href="https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage?view=sql-server-ver16#command-line-syntax">SqlPackage.exe</a> and the syntax needed will be something similar to</p>
<pre tabindex="0"><code>sqlpackage /Action:Extract /SourceConnectionString:&#34;Server=tcp:{instance},{port};Initial Catalog={databasename};TrustServerCertificate=True;integrated security=true;&#34; /TargetFile:{projectname} /p:ExtractTarget=SchemaObjectType
</code></pre><p>This command will generate an database project in a folder of the name specified using <code>/TargetFile</code>, where each SQL object has it&rsquo;s own file containing the appropriate SQL CREATE script.</p>
<p>The &lsquo;magic&rsquo; that took me too long to fine was that the <code>/p:ExtractTarget=SchemaObjectType</code> parameter is required. This instructs <strong>SQLpackage</strong> to generate a project structure and not extract to a single SQL file or DACPAC.</p>
<p>I am not sure whether my internet search powers were weak, or if this usage is just poorly documented, but I hope this post save future me, and others, some time.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why are my Azure DevOps Pipeline cache hits missing</title>
      <link>https://blog.richardfennell.net/posts/why-is-my-azure-devops-pipeline-cache-missing/</link>
      <pubDate>Wed, 05 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-is-my-azure-devops-pipeline-cache-missing/</guid>
      <description>&lt;p&gt;I have blogged in the past about &lt;a href=&#34;https://blog.richardfennell.net/posts/caching-nvd-dependancies/&#34;&gt;Caching NVD Vulnerability Dependency data on hosted Azure DevOps Pipeline agents&lt;/a&gt;. Using the cache is a great way to speed up slow builds.&lt;/p&gt;
&lt;p&gt;However, today I was surprised to find I was getting cache misses on my pipeline, even though I was sure the cache should have been hit.&lt;/p&gt;
&lt;p&gt;There are rules over how the cache is used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The cache is specific to a pipeline definition, so there is no sharing of the cache between pipeline definitions&lt;/li&gt;
&lt;li&gt;The cache is only created if the pipeline is successful (running the post run tasks)&lt;/li&gt;
&lt;li&gt;The cache only lasts 7 days&lt;/li&gt;
&lt;li&gt;but what I had not realised was the cache is also specific to the branch in a not so obvious way.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My pipeline was triggered off a PR, so the cache was being created on the &amp;lsquo;branch&amp;rsquo; PR #123. This was working as expected, all runs of the PR triggered build used the cache after the initial run. However, if I manually triggered pipeline run of the same branch as the PR was using, there was a cache miss.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have blogged in the past about <a href="https://blog.richardfennell.net/posts/caching-nvd-dependancies/">Caching NVD Vulnerability Dependency data on hosted Azure DevOps Pipeline agents</a>. Using the cache is a great way to speed up slow builds.</p>
<p>However, today I was surprised to find I was getting cache misses on my pipeline, even though I was sure the cache should have been hit.</p>
<p>There are rules over how the cache is used:</p>
<ul>
<li>The cache is specific to a pipeline definition, so there is no sharing of the cache between pipeline definitions</li>
<li>The cache is only created if the pipeline is successful (running the post run tasks)</li>
<li>The cache only lasts 7 days</li>
<li>but what I had not realised was the cache is also specific to the branch in a not so obvious way.</li>
</ul>
<p>My pipeline was triggered off a PR, so the cache was being created on the &lsquo;branch&rsquo; PR #123. This was working as expected, all runs of the PR triggered build used the cache after the initial run. However, if I manually triggered pipeline run of the same branch as the PR was using, there was a cache miss.</p>
<p>As far as the Azure DevOps cache task is concerned, the branch used for PR #123 and a manual run off the underlying branch used in the PR are different things, so the cache created by the PR was not being used by the manual branch build.</p>
<p>That explains a few slow builds I have had.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Personal Access Tokens (PATs) are not your friends</title>
      <link>https://blog.richardfennell.net/posts/pats-are-not-your-friends/</link>
      <pubDate>Fri, 22 Mar 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/pats-are-not-your-friends/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;Programmatic connection to Azure DevOps cannot be done with your Active Directory credentials. This is because this involves a dialog being shown, and these days usually an MFA check too.&lt;/p&gt;
&lt;p&gt;Historically, the solution to this problem was to enable &lt;a href=&#34;https://aka.ms/vstspolicyaltauth&#34;&gt;Alternate Credentials&lt;/a&gt;, which could be passed as username and password, without the dialog being shown. However, the use of these has been deprecated since 2020, and &lt;a href=&#34;https://devblogs.microsoft.com/devops/final-notice-of-alternate-credentials-deprecation/&#34;&gt;they have been completely removed since Jan 2024&lt;/a&gt;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>Programmatic connection to Azure DevOps cannot be done with your Active Directory credentials. This is because this involves a dialog being shown, and these days usually an MFA check too.</p>
<p>Historically, the solution to this problem was to enable <a href="https://aka.ms/vstspolicyaltauth">Alternate Credentials</a>, which could be passed as username and password, without the dialog being shown. However, the use of these has been deprecated since 2020, and <a href="https://devblogs.microsoft.com/devops/final-notice-of-alternate-credentials-deprecation/">they have been completely removed since Jan 2024</a>.</p>
<p>So for a number of years the recommended way to programmatically authenticate with Azure DevOps has been to use a <a href="https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&amp;tabs=Windows">Personal Access Token (PAT)</a>. However, PATs are not without their own issues.</p>
<h2 id="the-problem-with-pats">The problem with PATs</h2>
<p>The most critical problem is that PATs expire. By default they expire after 30 days, and the longest you can set them to expire after is a maximum of 1 year. This means that you have to remember to renew them, and if you forget then your automation that rely on them will stop working.</p>
<p>Obviously it is very tempting to just set the expiry to 1 year, but this is not the best idea. The longer the expiry the more time there is for the PAT to be compromised. Just like SSL certificates, or any access token, the shorter period before the expiry the better.</p>
<p>Of course like any authentication and permission model you can grant too many permissions to a PAT, and then if it is compromised you have a problem, but that is on you not following the principle of least privilege, not a fundamental issue of PATs.</p>
<h2 id="but-what-is-the-alternative">But what is the alternative?</h2>
<p>Azure DevOps now offers support for <a href="https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops">Managed Identity &amp; Service Principals</a>. These are a much better way to provide a means to programmatically authenticate. Critically, they do not expire, and the tokens they generate for the actual connections are short lived, and you can of course grant them privileges in line with the principle of least privilege.</p>
<p>When used to access Azure DevOps resource, the only downside is they consume a basic user license.</p>
<p>This form of authentication is perfect for use in automation from locations like <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-identity-based-connections-tutorial">Azure Functions</a>, or from the <a href="https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli-managed-identity">Az CLI</a>.</p>
<h2 id="what-about-github">What about GitHub?</h2>
<p>The issue of PATs is not just one for Azure DevOps, the same problems are true for GitHub. On GitHub the solution is to use a <a href="https://devopsjournal.io/blog/2022/01/03/GitHub-Tokens">GitHub App to authenticate with the GitHub API</a>, and then use the short term token it can generate to authenticate with the GitHub API.</p>
<h2 id="what-about-other-outgoing-services-from-azure-devops">What about other outgoing services from Azure DevOps?</h2>
<p>Another place you will see a move away from expiring secrets in Azure DevOps are Service Connections to services to such as Azure. You can now use <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#create-an-azure-resource-manager-service-connection-using-workload-identity-federation">Workload Identity Federation</a> to authenticate with Azure.</p>
<p>Converting to use Workload Identity Federation is simple, you just press a button in the Azure DevOps UI, and it will create a Service Principal in Azure AD, and then update the Service Connection to use this to authenticate with Azure.</p>
<p><img alt="Swap to Workload Identity Federation" loading="lazy" src="/images/rfennell/pat-serviceprinciple.png"></p>
<p>Once this is done there will be no need to remember to renew the Service Connection, as it will use the Service Principal to authenticate, and this does not expire.</p>
<h2 id="summary">Summary</h2>
<p>So, when using Azure DevOps in many cases you can now avoid the use of PATs, and hence the need to remember to renew them. Giving you one less thing to remember to do, and one less thing to go wrong.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Don&#39;t forget to commit your configuration file</title>
      <link>https://blog.richardfennell.net/posts/dont-forget-to-commit-your-configuration-file/</link>
      <pubDate>Wed, 20 Mar 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/dont-forget-to-commit-your-configuration-file/</guid>
      <description>&lt;p&gt;It is a major effort, often unfortunately ignored, keeping the dependencies in an open source project up to date. This was highlighted in &lt;a href=&#34;https://dev.to/jessehouwing/security-state-of-the-azure-devops-marketplace-5bil&#34;&gt;Jesse Houwing&amp;rsquo;s post on the state of the Azure DevOps Marketplace&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since reading this I have made much more of an effort to keep &lt;a href=&#34;https://marketplace.visualstudio.com/search?term=fennell&amp;amp;target=AzureDevOps&amp;amp;category=All%20categories&amp;amp;sortBy=Relevance&#34;&gt;my Azure DevOps Extensions&lt;/a&gt; up to date. &lt;a href=&#34;https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates&#34;&gt;Dependabot&lt;/a&gt; generated PRs have been a great help in this regard. Creating PRs for vulnerabilities and out of date dependencies.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>It is a major effort, often unfortunately ignored, keeping the dependencies in an open source project up to date. This was highlighted in <a href="https://dev.to/jessehouwing/security-state-of-the-azure-devops-marketplace-5bil">Jesse Houwing&rsquo;s post on the state of the Azure DevOps Marketplace</a>.</p>
<p>Since reading this I have made much more of an effort to keep <a href="https://marketplace.visualstudio.com/search?term=fennell&amp;target=AzureDevOps&amp;category=All%20categories&amp;sortBy=Relevance">my Azure DevOps Extensions</a> up to date. <a href="https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates">Dependabot</a> generated PRs have been a great help in this regard. Creating PRs for vulnerabilities and out of date dependencies.</p>
<p>However, no level of AI can protect you from stupidity. Recently, as part of addressing a vulnerability via a refactoring to change testing framework, I wasted far too long trying to work out why all my test passed in my Codespace, but failed in the CI build. I had forgotten to commit the <code>jest.config.js</code> file as my <code>.gitignore</code>was set to ignore all <code>.JS</code> files as I was working in TypeScript.</p>
<p>Very frustration, but a good reminder to always check the files you are committing are the ones you expect.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Parsing error with Azure Bicep files in SonarQube</title>
      <link>https://blog.richardfennell.net/posts/parsing-error-with-azure-bicep-files-insonarqube/</link>
      <pubDate>Tue, 19 Mar 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/parsing-error-with-azure-bicep-files-insonarqube/</guid>
      <description>&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;We saw an issue with our SonarQube 10.3 Developer Edition (that is running as a Docker image hosted in Azure) when it was doing the analysis of a project that included Azure Bicep files.&lt;/p&gt;
&lt;p&gt;The Azure DevOps pipeline that triggered the SonarQube analysis was not failing, but within the SonarQube analysis step an error was reported in the task log&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;INFO: Sensor IaC AzureResourceManager Sensor is restricted to changed files only
INFO: 1 source file to be analyzed
##[error]ERROR: Cannot parse &amp;#39;AzureServices/QueryPack.bicep:89:1&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;the-solution&#34;&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Turns out the problem was related to parsing Bicep files for App Insights Query packs.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-issue">The Issue</h2>
<p>We saw an issue with our SonarQube 10.3 Developer Edition (that is running as a Docker image hosted in Azure) when it was doing the analysis of a project that included Azure Bicep files.</p>
<p>The Azure DevOps pipeline that triggered the SonarQube analysis was not failing, but within the SonarQube analysis step an error was reported in the task log</p>
<pre tabindex="0"><code>INFO: Sensor IaC AzureResourceManager Sensor is restricted to changed files only
INFO: 1 source file to be analyzed
##[error]ERROR: Cannot parse &#39;AzureServices/QueryPack.bicep:89:1&#39;
</code></pre><h2 id="the-solution">The Solution</h2>
<p>Turns out the problem was related to parsing Bicep files for App Insights Query packs.</p>
<p>If the Bicep resource for the query contains a <code>body</code> property that starts with a comment e.g.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bicep" data-lang="bicep"><span class="line"><span class="cl"><span class="kd">resource</span><span class="w"> </span><span class="nv">querypacks_DefaultQueryPack</span><span class="w"> </span><span class="s">&#39;microsoft.operationalInsights/querypacks/queries@2019-09-01-preview&#39;</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">parent</span><span class="p">:</span><span class="w"> </span><span class="nv">QueryPack</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">name</span><span class="p">:</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">properties</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">displayName</span><span class="p">:</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">description</span><span class="p">:</span><span class="w"> </span><span class="p">..</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">body</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;// 35 is ABC\r\n// 40 is XYZ \r\nrequests\r\n| where name has &#34;myfacade.svc&#34;\r\n| order by timestamp desc\r\n| where name !has &#34;GET&#34;\r\n| summarize count() by name, resultCode\r\n| render columnchart&#39;</span><span class="w">
</span></span></span></code></pre></div><p>We get the error <code>##[error]ERROR: Cannot parse 'AzureServices/QueryPack.bicep:89:1</code></p>
<p>We can fix this by not starting the <code>body</code> with a comment, just moving the comment to the end of the <code>body</code> i.e.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bicep" data-lang="bicep"><span class="line"><span class="cl"><span class="kd">resource</span><span class="w"> </span><span class="nv">querypacks_DefaultQueryPack</span><span class="w"> </span><span class="s">&#39;microsoft.operationalInsights/querypacks/queries@2019-09-01-preview&#39;</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">parent</span><span class="p">:</span><span class="w"> </span><span class="nv">QueryPack</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">name</span><span class="p">:</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">properties</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">displayName</span><span class="p">:</span><span class="w"> </span><span class="p">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">description</span><span class="p">:</span><span class="w"> </span><span class="p">..</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">body</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;requests\r\n| where name has &#34;myfacade.svc&#34;\r\n| order by timestamp desc\r\n| where name !has &#34;GET&#34;\r\n| summarize count() by name, resultCode\r\n| render columnchart\r\n// 35 is ABC\r\n// 40 is XYZ&#39;</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Site fails to render when updating Hugo version</title>
      <link>https://blog.richardfennell.net/posts/site-fails-to-render-when-updating-hugo-version/</link>
      <pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/site-fails-to-render-when-updating-hugo-version/</guid>
      <description>&lt;h1 id=&#34;the-issue&#34;&gt;The Issue&lt;/h1&gt;
&lt;p&gt;This site was built using &lt;a href=&#34;https://gohugo.io/&#34;&gt;Hugo, a static site generator&lt;/a&gt;. I recently tried to do a long overdue update the version of Hugo from 0.108 to the current 0.122 version.&lt;/p&gt;
&lt;p&gt;I had not expected any problems, but found that the site failed to render, but with no error message all I saw was&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;hugo.exe server --logLevel info
Start building sites …
hugo v0.122.0-b9a03bd59d5f71a529acb3e33f995e0ef332b3aa+extended windows/amd64 BuildDate=2024-01-26T15:54:24Z VendorInfo=gohugoio

INFO  copy static: syncing static files to \
INFO  build: running step &amp;#34;process&amp;#34; duration &amp;#34;97.3263ms&amp;#34;
INFO  build: running step &amp;#34;assemble&amp;#34; duration &amp;#34;335.1476ms&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I normally would have expected to see a &lt;code&gt;INFO  build: running step &amp;quot;render&amp;quot;&lt;/code&gt; line or an error, but got nothing, irrespective of the log level I set.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="the-issue">The Issue</h1>
<p>This site was built using <a href="https://gohugo.io/">Hugo, a static site generator</a>. I recently tried to do a long overdue update the version of Hugo from 0.108 to the current 0.122 version.</p>
<p>I had not expected any problems, but found that the site failed to render, but with no error message all I saw was</p>
<pre tabindex="0"><code>hugo.exe server --logLevel info
Start building sites …
hugo v0.122.0-b9a03bd59d5f71a529acb3e33f995e0ef332b3aa+extended windows/amd64 BuildDate=2024-01-26T15:54:24Z VendorInfo=gohugoio

INFO  copy static: syncing static files to \
INFO  build: running step &#34;process&#34; duration &#34;97.3263ms&#34;
INFO  build: running step &#34;assemble&#34; duration &#34;335.1476ms&#34;
</code></pre><p>I normally would have expected to see a <code>INFO  build: running step &quot;render&quot;</code> line or an error, but got nothing, irrespective of the log level I set.</p>
<h1 id="the-solution">The Solution</h1>
<p>As I had no error message, and found nothing of use in the Hugo resources, I decide my only option was to roll back to a previous version of Hugo until I could get the site to render.</p>
<p>So I downloaded various versions and in the end found that this site rendered on 0.116 but not 0.117.</p>
<p>At this point I knew that some check being done in 0.117 was the issue, and this had to be due to my page content.</p>
<p>So the next step was to use a bisect test pattern to find the problem pages i.e. delete half the pages, if the problem does not occur you know the problem exists in the other half, if it is still present it is in the half you deleted.</p>
<p>By repeating this process I was able to find the problem pages. The issue turned out to be remotely hosted images that could not be found referenced in older blog posts. It seem that the Hugo render now checks for the existence of images in the content, and if they are missing the site fails to render.</p>
<p>Anyway after editing the pages to remove the missing image markdown the site rendered correctly in 0.117 and 0.122.</p>
<p>It is just a shame the logging was not more helpful.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting a BadGateway error when trying to create Azure DevOps Work items using Power Automated</title>
      <link>https://blog.richardfennell.net/posts/getting-a-badgateway-error-when-trying-to-create-azure-devops-work-items-using-power-automated/</link>
      <pubDate>Tue, 13 Feb 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-a-badgateway-error-when-trying-to-create-azure-devops-work-items-using-power-automated/</guid>
      <description>&lt;p&gt;I was recently trying to create an Azure DevOps work item when an email is received using the &lt;a href=&#34;https://powerautomate.microsoft.com/en-us/templates/details/29ef92b0630d11e6ac13ff9c624ade25/create-an-azure-devops-work-item-when-email-arrives-with-bug-in-subject/&#34;&gt;Power Automate &amp;lsquo;Create an Azure DevOps work item when email arrives with &amp;lsquo;Bug&amp;rsquo; in subject&amp;rsquo; template&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The flow created without issue, and all the drop downs were correctly populated with O365 and Azure DevOps values as expected.&lt;/p&gt;
&lt;p&gt;However, when the flow ran, on receiving an email to the correct inbox, it failed with a BadGateway error.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I was recently trying to create an Azure DevOps work item when an email is received using the <a href="https://powerautomate.microsoft.com/en-us/templates/details/29ef92b0630d11e6ac13ff9c624ade25/create-an-azure-devops-work-item-when-email-arrives-with-bug-in-subject/">Power Automate &lsquo;Create an Azure DevOps work item when email arrives with &lsquo;Bug&rsquo; in subject&rsquo; template</a>.</p>
<p>The flow created without issue, and all the drop downs were correctly populated with O365 and Azure DevOps values as expected.</p>
<p>However, when the flow ran, on receiving an email to the correct inbox, it failed with a BadGateway error.</p>
<p>The problem turned out to be that the Work Item type I had set the flow to create was not available in the project I was trying to create it in (it was disabled in the process template).</p>
<p>So, if you get a BadGateway (502) error in Power Automated Flow, do not assume Azure DevOps is down, but rather you have a bad/invalid payload in the call being made to the Azure DevOps API. I think what is happening is the Azure DevOps API is returning a 4xx bad data error (which may have a bit more detail, but I doubt it from my past REST API experience), but you cannot see it as Power Automate is hiding that error with its own 502 BadGateway error.</p>
<p>So the tip is check the details of your payload and ensure it is valid for the API you are calling.</p>
]]></content:encoded>
    </item>
    <item>
      <title>SonarQube Docker Container will not start</title>
      <link>https://blog.richardfennell.net/posts/sonarqube-container-will-not-start/</link>
      <pubDate>Wed, 27 Sep 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/sonarqube-container-will-not-start/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;We run our SonarQube instance in a &lt;a href=&#34;https://devblogs.microsoft.com/premier-developer/sonarqube-hosted-on-azure-app-service/&#34;&gt;Docker container hosted in an Azure Web App Service&lt;/a&gt;. Today, with no notice, it failed. We did the obvious, just tried to restart it and the startup process failed.&lt;/p&gt;
&lt;p&gt;Looking at the Azure Web App&amp;rsquo;s Log Stream we could see the following error repeated on each restart attempt&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:00.797Z INFO - Starting multi-container app..
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:01.024Z INFO - Pulling image: sonarqube:10.1-developer
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.100Z INFO - 10.1-developer Pulling from library/sonarqube
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.291Z INFO - Digest: sha256:45e7cf02e037b00028d20556a91111f8ae8ae2b2803e516cb0665dd605a6d8b2
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.292Z INFO - Status: Image is up to date for sonarqube:10.1-developer
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.326Z INFO - Pull Image successful, Time taken: 0 Minutes and 1 Seconds
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.344Z INFO - Starting container for site
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.346Z INFO - docker run -d -p 8289:9000 --name bmsonarqubeprodwebsite_sonarqube_0_ec31111b -e WEBSITES_ENABLE_APP_SERVICE_STORAGE=false -e WEBSITE_SITE_NAME=bmsonarqubeprodwebsite -e WEBSITE_AUTH_ENABLED=False -e WEBSITE_ROLE_INSTANCE_ID=0 -e WEBSITE_HOSTNAME=bmsonarqubeprodwebsite.azurewebsites.net -e WEBSITE_INSTANCE_ID=6d27706a6b4eb56feec6ef57ab9b360923c5761cabb3fb52eb6fc5f4cdfbace3 -e WEBSITE_USE_DIAGNOSTIC_SERVER=False sonarqube:10.1-developer -Dsonar.search.javaAdditionalOpts=-Dnode.store.allow_mmap=false
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:30:02.347Z INFO - Logging is not enabled for this container.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:31:09 No new trace in the past 1 min(s).
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:32:09 No new trace in the past 2 min(s).
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:33:09 No new trace in the past 3 min(s).
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;2023-09-27T15:33:52.465Z ERROR - multi-container unit was not started successfully
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-solution&#34;&gt;The Solution&lt;/h2&gt;
&lt;p&gt;Something in the back of my mind, from when we ran an on-premises SonarQube instance, made me think of a corrupt ElasticSearch index.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2>
<p>We run our SonarQube instance in a <a href="https://devblogs.microsoft.com/premier-developer/sonarqube-hosted-on-azure-app-service/">Docker container hosted in an Azure Web App Service</a>. Today, with no notice, it failed. We did the obvious, just tried to restart it and the startup process failed.</p>
<p>Looking at the Azure Web App&rsquo;s Log Stream we could see the following error repeated on each restart attempt</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">2023-09-27T15:30:00.797Z INFO - Starting multi-container app..
</span></span><span class="line"><span class="cl">2023-09-27T15:30:01.024Z INFO - Pulling image: sonarqube:10.1-developer
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.100Z INFO - 10.1-developer Pulling from library/sonarqube
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.291Z INFO - Digest: sha256:45e7cf02e037b00028d20556a91111f8ae8ae2b2803e516cb0665dd605a6d8b2
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.292Z INFO - Status: Image is up to date for sonarqube:10.1-developer
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.326Z INFO - Pull Image successful, Time taken: 0 Minutes and 1 Seconds
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.344Z INFO - Starting container for site
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.346Z INFO - docker run -d -p 8289:9000 --name bmsonarqubeprodwebsite_sonarqube_0_ec31111b -e WEBSITES_ENABLE_APP_SERVICE_STORAGE=false -e WEBSITE_SITE_NAME=bmsonarqubeprodwebsite -e WEBSITE_AUTH_ENABLED=False -e WEBSITE_ROLE_INSTANCE_ID=0 -e WEBSITE_HOSTNAME=bmsonarqubeprodwebsite.azurewebsites.net -e WEBSITE_INSTANCE_ID=6d27706a6b4eb56feec6ef57ab9b360923c5761cabb3fb52eb6fc5f4cdfbace3 -e WEBSITE_USE_DIAGNOSTIC_SERVER=False sonarqube:10.1-developer -Dsonar.search.javaAdditionalOpts=-Dnode.store.allow_mmap=false
</span></span><span class="line"><span class="cl">2023-09-27T15:30:02.347Z INFO - Logging is not enabled for this container.
</span></span><span class="line"><span class="cl">Please use https://aka.ms/linux-diagnostics to enable logging to see container logs here.
</span></span><span class="line"><span class="cl">2023-09-27T15:31:09 No new trace in the past 1 min(s).
</span></span><span class="line"><span class="cl">2023-09-27T15:32:09 No new trace in the past 2 min(s).
</span></span><span class="line"><span class="cl">2023-09-27T15:33:09 No new trace in the past 3 min(s).
</span></span><span class="line"><span class="cl">2023-09-27T15:33:52.465Z ERROR - multi-container unit was not started successfully
</span></span></code></pre></div><h2 id="the-solution">The Solution</h2>
<p>Something in the back of my mind, from when we ran an on-premises SonarQube instance, made me think of a corrupt ElasticSearch index.</p>
<p>Using Azure Cloud Storage Explorer, I connected to the <code>sonarqube-data</code> file share and deleted the <code>ES8</code> data folder</p>
<p>I then restarted the container and it started OK, recreating the <code>ES8</code> folder and re-indexing the SonarQube content.</p>
<p>Thus far all appears to be OK.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Moving my Azure DevOps Pipeline generated social posts to Azure Logic Apps</title>
      <link>https://blog.richardfennell.net/posts/moving-my-azure-devops-pipeline-social-posts-to-logic-apps/</link>
      <pubDate>Tue, 25 Apr 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/moving-my-azure-devops-pipeline-social-posts-to-logic-apps/</guid>
      <description>&lt;p&gt;I &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/social-media-posts-after-migrating-from-wordpress-to-hugo/&#34;&gt;posted&lt;/a&gt; a while ago about how I had automated the generation of social media posts for my static Hugo based website using Azure Logic Apps.&lt;/p&gt;
&lt;p&gt;The other place I auto-generate social media posts is from releases via my project&amp;rsquo;s Azure DevOps Pipeline builds. These use a YAML Pipeline Template that calls a &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=petergroenewegen.PeterGroenewegen-Xpirit-Vsts-Release-Twitter&#34;&gt;Marketplace task to post to Twitter&lt;/a&gt; and a PowerShell task to  &lt;code&gt;Invoke-WebRequest&lt;/code&gt; to post to Mastodon.&lt;/p&gt;
&lt;p&gt;Recently the Twitter task started to fail, and given the recent changes to the Twitter API with the move to the &lt;a href=&#34;https://developer.twitter.com/en/support/twitter-api/v2&#34;&gt;V2 API&lt;/a&gt;, I decided a new solution was required.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I <a href="https://blogs.blackmarble.co.uk/rfennell/social-media-posts-after-migrating-from-wordpress-to-hugo/">posted</a> a while ago about how I had automated the generation of social media posts for my static Hugo based website using Azure Logic Apps.</p>
<p>The other place I auto-generate social media posts is from releases via my project&rsquo;s Azure DevOps Pipeline builds. These use a YAML Pipeline Template that calls a <a href="https://marketplace.visualstudio.com/items?itemName=petergroenewegen.PeterGroenewegen-Xpirit-Vsts-Release-Twitter">Marketplace task to post to Twitter</a> and a PowerShell task to  <code>Invoke-WebRequest</code> to post to Mastodon.</p>
<p>Recently the Twitter task started to fail, and given the recent changes to the Twitter API with the move to the <a href="https://developer.twitter.com/en/support/twitter-api/v2">V2 API</a>, I decided a new solution was required.</p>
<p>I realised the simplest solution was to use another Azure Logic App sharing the connectors I had already created for my website posts. To make this change, my post build YAML template was greatly simplified to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">buildNumber</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">extensionName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">socialmediaLogicAppURL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span><span class="w">  
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># Update the build number variable so the next build will be the next minor version</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">richardfennellBM.BM-VSTS-BuildUpdating-Tasks-DEV.BuildVariableTask-Task.BuildVariableTask@1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Update Build Variable&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">variable</span><span class="p">:</span><span class="w"> </span><span class="l">Minor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">Autoincrement</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">usedefaultcreds</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># Get the PR title and hence the reason for the release</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">richardfennellBM.BM-VSTS-ArtifactDescription-Tasks-DEV.ArtifactDescriptionTask.ArtifactDescriptionTask@1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Get Git Artifact PR Reason&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">OutputText</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;OutputedText&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># Post to the Logic App to create various social media posts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">pwsh</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">   $msg = &#34;I have just released Version ${{parameters.buildNumber}} of my Azure DevOps Pipeline ${{parameters.extensionName}} http://bit.ly/VSTS-RF $(OutputedText) &#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">   write-host &#34;Posting message: $msg&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">   $uri = &#34;${{parameters.socialmediaLogicAppURL}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">   $body = &#34;{ `&#34;Message`&#34;: `&#34;$msg`&#34;}&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">   
</span></span></span><span class="line"><span class="cl"><span class="sd">   Invoke-WebRequest -Uri $uri -Method POST -Body $body -Headers $headers -ContentType &#34;application/json&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Create social media posts about new release&#39;</span><span class="w">
</span></span></span></code></pre></div><p>I can now just update the Logic App to change to which social media platforms posts are generated.</p>
<p><img alt="Logic App Design" loading="lazy" src="/images/rfennell/LogicAppScreenShot1.png"></p>
<p>So, arguably a better solution as I am using each tool for the job it was designed for i.e handing off the orchestration of external systems to Logic Apps as opposed to managing it in the build pipeline.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A more secure alternative to PAT tokens for accessing Azure DevOps Programmatically</title>
      <link>https://blog.richardfennell.net/posts/a-more-secure-alternative-to-pat-tokens-for-azure-devops/</link>
      <pubDate>Fri, 21 Apr 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/a-more-secure-alternative-to-pat-tokens-for-azure-devops/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;When working with Azure DevOps, you may need to access the &lt;a href=&#34;https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.1&#34;&gt;REST API&lt;/a&gt; if you wish to perform scripted tasks such as creating work items, or generating reports. Historically, you had to use a &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&amp;amp;tabs=Windows&#34;&gt;Personal Access Token (PAT)&lt;/a&gt; to do this.&lt;/p&gt;
&lt;p&gt;If you look in my &lt;a href=&#34;https://github.com/rfennell/AzureDevOpsPowershell&#34;&gt;repo of useful Azure DevOps PowerShell scripts&lt;/a&gt; you will find all the scripts make use of a function that creates an authenticated &lt;code&gt;WebClient&lt;/code&gt; object using a passed in PAT token.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>When working with Azure DevOps, you may need to access the <a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.1">REST API</a> if you wish to perform scripted tasks such as creating work items, or generating reports. Historically, you had to use a <a href="https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&amp;tabs=Windows">Personal Access Token (PAT)</a> to do this.</p>
<p>If you look in my <a href="https://github.com/rfennell/AzureDevOpsPowershell">repo of useful Azure DevOps PowerShell scripts</a> you will find all the scripts make use of a function that creates an authenticated <code>WebClient</code> object using a passed in PAT token.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Get-WebClient</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">CmdletBinding</span><span class="p">()]</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$pat</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$webclient</span> <span class="p">=</span> <span class="nb">new-object</span> <span class="n">System</span><span class="p">.</span><span class="py">Net</span><span class="p">.</span><span class="py">WebClient</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$webclient</span><span class="p">.</span><span class="py">Encoding</span> <span class="p">=</span> <span class="p">[</span><span class="no">System.Text.Encoding</span><span class="p">]::</span><span class="n">UTF8</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$encodedPat</span> <span class="p">=</span> <span class="p">[</span><span class="no">System.Convert</span><span class="p">]::</span><span class="n">ToBase64String</span><span class="p">([</span><span class="no">System.Text.Encoding</span><span class="p">]::</span><span class="n">UTF8</span><span class="p">.</span><span class="py">GetBytes</span><span class="p">(</span><span class="s2">&#34;:</span><span class="nv">$pat</span><span class="s2">&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$webclient</span><span class="p">.</span><span class="py">Headers</span><span class="p">.</span><span class="py">Add</span><span class="p">(</span><span class="s2">&#34;Authorization&#34;</span><span class="p">,</span> <span class="s2">&#34;Basic </span><span class="nv">$encodedPat</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$webclient</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>which is used thus</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$wc</span> <span class="p">=</span> <span class="nb">Get-WebClient</span> <span class="n">-pat</span> <span class="s2">&#34;a-pat-string&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$result</span><span class="p">=</span> <span class="nv">$wc</span><span class="p">.</span><span class="py">DownloadString</span><span class="p">(</span><span class="s2">&#34;https://dev.azure.com/MyOrg/MyProject/_apis/build/builds&#34;</span><span class="p">)</span> <span class="p">|</span> <span class="nb">ConvertFrom-Json</span>
</span></span><span class="line"><span class="cl"><span class="nv">$result</span><span class="p">.</span><span class="py">value</span>
</span></span></code></pre></div><p>The problem with this approach is that the PAT tokens have to be managed. They expire after a period of time, so have to be regenerated and also, as they are in effect passwords, need to be stored securely.</p>
<h2 id="a-better-approach">A better approach</h2>
<p>A newly available and better approach is to use an <a href="https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops">Azure AD App Service Principle</a> to authenticate to Azure DevOps. This addresses the issues with PAT tokens, as the App Service Principles do not expire and are defined securely in Azure AD.</p>
<p>The basic setup is as follows</p>
<ol>
<li>Create a new Azure AD App</li>
<li>Add the new App Service Principle to the Azure DevOps organisation as a user</li>
<li>Grant the App Service Principle the required permissions in Azure DevOps</li>
<li>Use the App Service Principle for programmatic authenticate to Azure DevOps e.g to the API</li>
</ol>
<p>The sample script hence becomes</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Get-WebClient</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">CmdletBinding</span><span class="p">()]</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$ClientID</span> <span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$Secret</span>   <span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$TenantID</span> 
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c"># This is a static value</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$AdoAppClientID</span> <span class="p">=</span> <span class="s2">&#34;499b84ac-1321-427f-aa17-267ca6975798/.default&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$loginUrl</span> <span class="p">=</span> <span class="s2">&#34;https://login.microsoftonline.com/</span><span class="nv">$tenantId</span><span class="s2">/oauth2/token&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$body</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">grant_type</span>    <span class="p">=</span> <span class="s2">&#34;client_credentials&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">client_id</span>     <span class="p">=</span> <span class="nv">$ClientID</span>
</span></span><span class="line"><span class="cl">        <span class="n">client_secret</span> <span class="p">=</span> <span class="nv">$Secret</span> 
</span></span><span class="line"><span class="cl">        <span class="n">resource</span>      <span class="p">=</span> <span class="nv">$AdoAppClientID</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$token</span> <span class="p">=</span> <span class="nb">Invoke-RestMethod</span> <span class="n">-Uri</span> <span class="nv">$loginUrl</span> <span class="n">-Method</span> <span class="n">POST</span> <span class="n">-Body</span> <span class="nv">$body</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nv">$webclient</span> <span class="p">=</span> <span class="nb">new-object</span> <span class="n">System</span><span class="p">.</span><span class="py">Net</span><span class="p">.</span><span class="py">WebClient</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$webclient</span><span class="p">.</span><span class="py">Encoding</span> <span class="p">=</span> <span class="p">[</span><span class="no">System.Text.Encoding</span><span class="p">]::</span><span class="n">UTF8</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$webclient</span><span class="p">.</span><span class="py">Headers</span><span class="p">.</span><span class="py">Add</span><span class="p">(</span><span class="s2">&#34;Authorization&#34;</span><span class="p">,</span> <span class="s2">&#34;Bearer </span><span class="p">$(</span><span class="nv">$token</span><span class="p">.</span><span class="n">access_token</span><span class="p">)</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nv">$webclient</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$wc</span> <span class="p">=</span> <span class="nb">Get-WebClient</span> <span class="n">-ClientID</span> <span class="s2">&#34;a string&#34;</span> <span class="n">-Secret</span> <span class="s2">&#34;a secret&#34;</span> <span class="n">-TenantID</span> <span class="s2">&#34;a tenant id&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$result</span><span class="p">=</span> <span class="nv">$wc</span><span class="p">.</span><span class="py">DownloadString</span><span class="p">(</span><span class="s2">&#34;https://dev.azure.com/MyOrg/MyProject/_apis/build/builds&#34;</span><span class="p">)</span> <span class="p">|</span> <span class="nb">ConvertFrom-Json</span>
</span></span><span class="line"><span class="cl"><span class="nv">$result</span><span class="p">.</span><span class="py">value</span>
</span></span></code></pre></div><p>Now, the observant amongst you will have noticed in this sample the <code>Get-WebClient</code> function still takes a secret, which is less than optimal. So, in most use-case it is recommended that the token is retrieved using a certificate, rather than a secret, but the process is basically the same. See the worked <a href="https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops">Microsoft example for details</a>.</p>
<p>The one potential downside of this approach is that the App Service Principle may require <a href="https://learn.microsoft.com/en-us/azure/devops/organizations/security/access-levels?view=azure-devops">a paid for Azure DevOps license</a>, but this is not always the case, it depends on the API calls you will be making.</p>
<p>Broadly speaking, calls to get Work Item details can be done with free stakeholder licenses, but others to get build or code details will probably require a basic license. However, remember you do get 5 free basic licenses, so there is a good chance you have one spare, or at worst they are only $6 a month.</p>
<p>So is this something that may make your programmatic access to Azure DevOps easier and more secure?</p>
]]></content:encoded>
    </item>
    <item>
      <title>Downloading NuGet packages with &#39;System.Net.WebClient&#39; from an Azure DevOps Artifact feed</title>
      <link>https://blog.richardfennell.net/posts/downloading-nuget-packages-with-system.net.webclient/</link>
      <pubDate>Sat, 01 Apr 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/downloading-nuget-packages-with-system.net.webclient/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;We use &lt;a href=&#34;https://github.com/VirtualEngine/Lability&#34;&gt;Lability&lt;/a&gt; to build Windows Server images for our test labs. Lability makes use of &lt;a href=&#34;https://docs.microsoft.com/en-us/powershell/dsc/overview&#34;&gt;Desired State Configuration&lt;/a&gt; (DSC) to build the VM images. Part of this process is for Lability to download DSC modules, as ZIP files, from a NuGet feed such as &lt;a href=&#34;https://www.powershellgallery.com/&#34;&gt;PowerShell Gallery&lt;/a&gt; to inject into the created VM image.&lt;/p&gt;
&lt;p&gt;Historically, we have stored our own private DSC modules on an internally hosted NuGet server. However, we wanted to move these modules to a private &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/artifacts/quickstarts/nuget?view=azure-devops&#34;&gt;Azure DevOps Artifacts feed&lt;/a&gt;. The problem was that Lability does not support downloading of DSC modules from Azure DevOps Artifact feeds, whether they are public or private, because of the way the package URLs are constructed.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>We use <a href="https://github.com/VirtualEngine/Lability">Lability</a> to build Windows Server images for our test labs. Lability makes use of <a href="https://docs.microsoft.com/en-us/powershell/dsc/overview">Desired State Configuration</a> (DSC) to build the VM images. Part of this process is for Lability to download DSC modules, as ZIP files, from a NuGet feed such as <a href="https://www.powershellgallery.com/">PowerShell Gallery</a> to inject into the created VM image.</p>
<p>Historically, we have stored our own private DSC modules on an internally hosted NuGet server. However, we wanted to move these modules to a private <a href="https://docs.microsoft.com/en-us/azure/devops/artifacts/quickstarts/nuget?view=azure-devops">Azure DevOps Artifacts feed</a>. The problem was that Lability does not support downloading of DSC modules from Azure DevOps Artifact feeds, whether they are public or private, because of the way the package URLs are constructed.</p>
<p>I am in the process of creating a PR for Lability to add support for Azure DevOps Artifact feeds, but I thought it worth this quick blog post on package URL formats as it might be useful to others.</p>
<h2 id="urls-for-powershell-gallery">URLs for PowerShell Gallery</h2>
<p>The URL format for downloading a package from the PowerShell Gallery is well known, being</p>
<pre tabindex="0"><code>https://www.powershellgallery.com/packages/&lt;package name&gt;/&lt;version&gt;
</code></pre><p>Enter this URL in a browser and the requested package will be downloaded</p>
<h2 id="urls-for-azure-devops-artifacts">URLs for Azure DevOps Artifacts</h2>
<p>You might well expect the format for downloading a package from Azure DevOps Artifacts to be</p>
<pre tabindex="0"><code>https://pkgs.dev.azure.com/&lt;AzDo Org&gt;/_packaging/&lt;Feed Name&gt;/nuget/v2/Packages/&lt;package name&gt;/&lt;version&gt;
</code></pre><p>but it is not.</p>
<p>Stackoverflow suggests the URL format is</p>
<pre tabindex="0"><code>https://pkgs.dev.azure.com/&lt;AzDo Org&gt;/_packaging/&lt;Feed Name&gt;/nuget/v2/Packages(id=&lt;Package name&gt;,version=&lt;version&gt;)
</code></pre><p>However, this does not download a package, but rather an XML manifest file. But from this manifest we can see a <code>&lt;content&gt;</code> element that contains the URL to the package, and it is an interesting format.</p>
<p>So for the manifest  URL</p>
<pre tabindex="0"><code>https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v2/Packages(id=xNetworking,version=5.7.0.0)
</code></pre><p>the content URL is</p>
<pre tabindex="0"><code>https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v2?id=xnetworking&amp;version=5.7.0
</code></pre><p>Note that</p>
<ul>
<li>the package name is lower case</li>
<li>the four part version number is replaced with a three part version number</li>
<li>the XML encoded <code>&amp;amp;</code> value in the manifest is replaced with a simple <code>&amp;</code></li>
</ul>
<p>The following gist is a function to build the required URL from the package name and version number and download the package as a ZIP file</p>
<script src="https://gist.github.com/rfennell/835575c96f54e27a3ba0816f2f8c2317.js"></script>
<p>Hope this helps someone and saves them some time.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Pinning specific Azure DevOps task versions</title>
      <link>https://blog.richardfennell.net/posts/pinning-specific-azure-devops-task-versions/</link>
      <pubDate>Wed, 08 Mar 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/pinning-specific-azure-devops-task-versions/</guid>
      <description>&lt;p&gt;I make every effort keep all my &lt;a href=&#34;https://marketplace.visualstudio.com/search?term=fennell&amp;amp;target=AzureDevOps&amp;amp;category=All%20categories&amp;amp;sortBy=Relevance&#34;&gt;Azure DevOps Pipeline extensions&lt;/a&gt; reliable as I know they are used by many people, but mistakes happen.&lt;/p&gt;
&lt;p&gt;Yesterday I released an updated version of my &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes&#34;&gt;ReleaseNotes task&lt;/a&gt; that introduced a bug if the pipeline produced no artifacts. I am pleased to say I have fixed the bug, and addressed this gap in my test coverage.&lt;/p&gt;
&lt;p&gt;However, this did mean for about 12 hours if you are using this task in a pipeline that did not produce artifacts, maybe one that just deployed consumed artifacts from other pipelines, you had a failing pipeline.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I make every effort keep all my <a href="https://marketplace.visualstudio.com/search?term=fennell&amp;target=AzureDevOps&amp;category=All%20categories&amp;sortBy=Relevance">Azure DevOps Pipeline extensions</a> reliable as I know they are used by many people, but mistakes happen.</p>
<p>Yesterday I released an updated version of my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes">ReleaseNotes task</a> that introduced a bug if the pipeline produced no artifacts. I am pleased to say I have fixed the bug, and addressed this gap in my test coverage.</p>
<p>However, this did mean for about 12 hours if you are using this task in a pipeline that did not produce artifacts, maybe one that just deployed consumed artifacts from other pipelines, you had a failing pipeline.</p>
<p>In this case all is not lost, as there is a feature of YAML pipelines I only recently discovered.</p>
<p>I knew you could select a specific major version of a task (as you can with Classic Builds and Releases) e.g.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">XplatGenerateReleaseNotes@3</span><span class="w">
</span></span></span></code></pre></div><p>or</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">XplatGenerateReleaseNotes@4</span><span class="w">
</span></span></span></code></pre></div><p>However, did you know you can also pin a specific version of a task e.g.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">XplatGenerateReleaseNotes@4.6.2</span><span class="w">
</span></span></span></code></pre></div><p>Thus allowing you to pick any version you wish, and not just the major version. A great way to lock down your pipelines to a known good version of a task, whether as a short term fix or a long term audit control.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting x86 .NET 3.x tests running on the latest Azure Devops hosted agents</title>
      <link>https://blog.richardfennell.net/posts/getting-x86-test-running-on-the-latest-azure-devops-hosted-agents/</link>
      <pubDate>Mon, 06 Mar 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-x86-test-running-on-the-latest-azure-devops-hosted-agents/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;At Black Marble we have our own private build agents, but they are built using the same &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/creating-hyper-v-hosted-azure-devops-private-agents-based-on-the-same-vm-images-as-used-by-microsoft-for-their-hosted-agents/&#34;&gt;Packer process as the Microsoft hosted ones&lt;/a&gt;. I recently rebuilt our agents to match the latest version of the hosted agents, and I ran into an issue with some .NET 3.1 based x86 MSTests. The tests were failing with the following error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;A total of 34 test files matched the specified pattern.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;##[error]Testhost process exited with error: A fatal error occurred. The required library hostfxr.dll could not be found.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;##[error]If this is a self-contained application, that library should exist in [E:\Agent\_work\1\s\src\Ux.Common.UnitTests\bin\x86\Release\netcoreapp3.1\].
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;##[error]If this is a framework-dependent application, install the runtime in the global location [C:\Program Files (x86)\dotnet] or use the DOTNET_ROOT(x86) environment variable to specify the runtime location or register the runtime location in [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation].
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When I checked the folder &lt;code&gt;C:\Program Files (x86)\dotnet&lt;/code&gt; it was not there, .NET 3.1 x86 was no longer present on the agent.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>At Black Marble we have our own private build agents, but they are built using the same <a href="https://blogs.blackmarble.co.uk/rfennell/creating-hyper-v-hosted-azure-devops-private-agents-based-on-the-same-vm-images-as-used-by-microsoft-for-their-hosted-agents/">Packer process as the Microsoft hosted ones</a>. I recently rebuilt our agents to match the latest version of the hosted agents, and I ran into an issue with some .NET 3.1 based x86 MSTests. The tests were failing with the following error:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">A total of 34 test files matched the specified pattern.
</span></span><span class="line"><span class="cl">##[error]Testhost process exited with error: A fatal error occurred. The required library hostfxr.dll could not be found.
</span></span><span class="line"><span class="cl">##[error]If this is a self-contained application, that library should exist in [E:\Agent\_work\1\s\src\Ux.Common.UnitTests\bin\x86\Release\netcoreapp3.1\].
</span></span><span class="line"><span class="cl">##[error]If this is a framework-dependent application, install the runtime in the global location [C:\Program Files (x86)\dotnet] or use the DOTNET_ROOT(x86) environment variable to specify the runtime location or register the runtime location in [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation].
</span></span></code></pre></div><p>When I checked the folder <code>C:\Program Files (x86)\dotnet</code> it was not there, .NET 3.1 x86 was no longer present on the agent.</p>
<p>This removal of the x86 .NET SDK from the Packer build appears to have occurred around the start of the year. As when I last rebuild out agents in December 2022, they still got the x86 .NET SDK installed.</p>
<h2 id="solution">Solution</h2>
<p>I found the solution in <a href="https://github.com/microsoft/azure-pipelines-tasks/issues/16501">this GitHub issue</a>, to use an Azure DevOps task to install the .NET 3.1 x86 SDK as part of my build pipeline</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">UseDotNet@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="l">Install .NET 3.1 x86 to support vstest.console.exe</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">packageType</span><span class="p">:</span><span class="w"> </span><span class="l">sdk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">3.</span><span class="l">x</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">PROCESSOR_ARCHITECTURE</span><span class="p">:</span><span class="w"> </span><span class="l">x86</span><span class="w">
</span></span></span></code></pre></div><p>However, I found that it was not enough to just install the x86 SDK in this manner.</p>
<p>The <code>UseDotNet@2</code> task installs the SDK into <code>C:\hostedtoolcache\windows\dotnet</code> and set the environment variable <code>DOTNET_ROOT</code> to point this folder, so the SDK can be found by tools that need it.</p>
<p>The problem is that <code>vstest.console.exe</code> does not seem to be aware of this environment variable, and even if <code>DOTNET_ROOT</code> is set it still looks in the default <code>C:\Program Files (x86)\dotnet</code> folder.</p>
<p>So I also needed to add the following PowerShell inline script to my build pipeline to also set the <code>DOTNET_ROOT(x86)</code> environment variable. This extra step was also detailed towards the start of <a href="https://github.com/microsoft/azure-pipelines-tasks/issues/16501">the same GitHub issue when discussing manual install based workarounds</a>, but I had initially missed it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">powershell</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        # Set the DOTNET_ROOT(x86) so vstest.console.exe can find the SDK
</span></span></span><span class="line"><span class="cl"><span class="sd">        # The UseDotNet@2 only sets the platform independant DOTNET_ROOT one that is not read by vstest.console.exe
</span></span></span><span class="line"><span class="cl"><span class="sd">        Write-Host &#34;Setting environment variable DOTNET_ROOT(x86)=$(Agent.ToolsDirectory)\dotnet&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        Write-Host &#34;##vso[task.setvariable variable=DOTNET_ROOT(x86)]$(Agent.ToolsDirectory)\dotnet&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Set `DOTNET_ROOT(x86)` environment variable&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   
</span></span></span></code></pre></div><p>So together these two steps allowed my x86 MSTest tests to run successfully again.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting &#34;cannot find path&#34; error using Install-Package</title>
      <link>https://blog.richardfennell.net/posts/getting-cannot-find-path-error-using-install-package/</link>
      <pubDate>Mon, 27 Feb 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-cannot-find-path-error-using-install-package/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I was recently trying to use PowerShelGet &lt;code&gt;Install-Package&lt;/code&gt; to install a module from an Azure DevOps Artifacts hosted PowerShell Gallery using the following script&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-powershell&#34; data-lang=&#34;powershell&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# For authentication use a PAT as the password, UID can be anything&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;$PATcreds&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Get-Credential&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;Register-PSRepository&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;-Name&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BM&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;-SourceLocation&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;https://pkgs.dev.azure.com/&amp;lt;org&amp;gt;/_packaging/PowerShell/nuget/v2&amp;#39;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;-PublishLocation&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;https://pkgs.dev.azure.com/&amp;lt;org&amp;gt;/_packaging/PowerShell/nuget/v2&amp;#39;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;-InstallationPolicy&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Trusted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;Install-Package&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BlackMarble&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;py&#34;&gt;Package&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;-Source&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BM&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;-Credential&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$PATcreds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script did not work I was getting the error&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Install-Package : Cannot find the path &amp;lsquo;C:\Users&amp;lt;user&amp;gt;\AppData\Local\Temp\936930114\BlackMarble.Package\BlackMarble.Package.0.3.79\BlackMarble.Package.psd1&amp;rsquo; because it does not exist.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;You could see a download progress bar that suggested the download had occurred, but no module was installed.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>I was recently trying to use PowerShelGet <code>Install-Package</code> to install a module from an Azure DevOps Artifacts hosted PowerShell Gallery using the following script</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># For authentication use a PAT as the password, UID can be anything</span>
</span></span><span class="line"><span class="cl"><span class="nv">$PATcreds</span> <span class="p">=</span> <span class="nb">Get-Credential</span> 
</span></span><span class="line"><span class="cl"><span class="nb">Register-PSRepository</span> <span class="n">-Name</span> <span class="n">BM</span> <span class="n">-SourceLocation</span> <span class="s1">&#39;https://pkgs.dev.azure.com/&lt;org&gt;/_packaging/PowerShell/nuget/v2&#39;</span> <span class="n">-PublishLocation</span> <span class="s1">&#39;https://pkgs.dev.azure.com/&lt;org&gt;/_packaging/PowerShell/nuget/v2&#39;</span> <span class="n">-InstallationPolicy</span> <span class="n">Trusted</span>
</span></span><span class="line"><span class="cl"><span class="nb">Install-Package</span> <span class="n">BlackMarble</span><span class="p">.</span><span class="py">Package</span> <span class="n">-Source</span> <span class="n">BM</span> <span class="n">-Credential</span> <span class="nv">$PATcreds</span>
</span></span></code></pre></div><p>The script did not work I was getting the error</p>
<blockquote>
<p>Install-Package : Cannot find the path &lsquo;C:\Users&lt;user&gt;\AppData\Local\Temp\936930114\BlackMarble.Package\BlackMarble.Package.0.3.79\BlackMarble.Package.psd1&rsquo; because it does not exist.</p></blockquote>
<p>You could see a download progress bar that suggested the download had occurred, but no module was installed.</p>
<h2 id="solution">Solution</h2>
<p>Turns out the answer was to not register the repository, but to use a URL in the <code>-Source</code> parameter to <code>Install-Package</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Install-Package</span> <span class="n">BlackMarble</span><span class="p">.</span><span class="py">Package</span>  <span class="n">-Source</span> <span class="n">https</span><span class="err">:</span><span class="p">//</span><span class="n">pkgs</span><span class="p">.</span><span class="py">dev</span><span class="p">.</span><span class="py">azure</span><span class="p">.</span><span class="n">com</span><span class="p">/&lt;</span><span class="n">org</span><span class="p">&gt;/</span><span class="n">_packaging</span><span class="p">/</span><span class="n">PowerShell</span><span class="p">/</span><span class="n">nuget</span><span class="p">/</span><span class="n">v2</span> <span class="n">-Credential</span> <span class="nv">$PATcreds</span>
</span></span></code></pre></div><p>It sort of makes sense that there could be a bug such that URLs work and aliases, added via a <code>register-psrepository</code>, do not. Maybe a bug?</p>
<p>However, more strangely I have found that if the alias is registered the the <code>Install-Package</code> fails even if a URL is used. The complete solution is therefore to first <code>unregister-psrepository</code> the alias that match the URL you wish to use before running the <code>Install-Package</code> command.</p>
<p>All very strange</p>
]]></content:encoded>
    </item>
    <item>
      <title>What happens when you link an Azure DevOps Variable Group to an Azure Key Vault?</title>
      <link>https://blog.richardfennell.net/posts/what-happens-when-you-link-a-variable-group-to-key-vault/</link>
      <pubDate>Mon, 13 Feb 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/what-happens-when-you-link-a-variable-group-to-key-vault/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;It is a really useful feature that you can expose &lt;a href=&#34;https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&amp;amp;tabs=yaml#link-secrets-from-an-azure-key-vault&#34;&gt;Key Vault stored secrets as Azure DevOps pipeline variables via a variable group&lt;/a&gt;, but what happens when you do this? And what can you do if you try to expose too many variables?&lt;/p&gt;
&lt;p&gt;I was recently working on a system where there was an increasing number of Key Vault secrets that were being exposed as variables via a variable group. This was working fine, until I started getting warnings in the following form on Windows based Azure DevOps agents:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>It is a really useful feature that you can expose <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&amp;tabs=yaml#link-secrets-from-an-azure-key-vault">Key Vault stored secrets as Azure DevOps pipeline variables via a variable group</a>, but what happens when you do this? And what can you do if you try to expose too many variables?</p>
<p>I was recently working on a system where there was an increasing number of Key Vault secrets that were being exposed as variables via a variable group. This was working fine, until I started getting warnings in the following form on Windows based Azure DevOps agents:</p>
<blockquote>
<p>Environment variable &lsquo;VSTS_SECRET_VARIABLES&rsquo; exceeds the maximum supported length. Environment variable length: 32993 , Maximum supported length: 32766</p></blockquote>
<p>It was flagged as a warning, but in reality the pipeline failed as none of the secrets were being exposed as variables.</p>
<h2 id="analysis">Analysis</h2>
<p>When you link a variable group to a Key Vault, the secrets are exposed as environment variables. This is automatically done by the pipeline agent running the <code>AzureKeyVault@1</code> task (interestingly not the newer <code>AzureKeyVault@2</code> task) prior to any other steps in the containing job. This task takes parameters for the Key Vault name and a comma separated list based filter for the secrets to expose. Both of these are derived from the settings of the variable group.</p>
<h2 id="workarounds">Workarounds</h2>
<p>The simplest workarounds are to either:</p>
<ul>
<li>Switch to a Linux based agent which has a much higher limit on the length of environment variables. A move that is not always possible depending on the other tasks in the job.</li>
<li>Or to reduce the number of secrets exposed as variables. This can be done by either removing secrets from the Key Vault or by removing secret mapping within the variable group. A valid solution, but one that requires manual management.</li>
</ul>
<p>This got me thinking, is there another way to build the secret filter list so it is more flexible than a manually managed comma separated list?</p>
<p>The answer is yes, you can use a PowerShell script to build the list of secrets to expose and then call the <code>AzureKeyVault</code> task directly.</p>
<p>Instead of exposing the key Vault secrets via the variable group, you could use the following tasks.</p>
<ul>
<li>A PowerShell script is used to convert a wildcard based filter  into a comma separated list of secrets(this does assume you have a consistent secret naming convention)</li>
<li>Then, use the same <code>AzureKeyVault</code> task, to expose the secrets as environment variables.</li>
</ul>
<script src="https://gist.github.com/rfennell/6edb0ea400f23fdeb0c188b4011caf29.js"></script>
]]></content:encoded>
    </item>
    <item>
      <title>Handling return values from Azure Functions in Hugo static website</title>
      <link>https://blog.richardfennell.net/posts/hugo-static-website-azure-functions-and-return-values/</link>
      <pubDate>Fri, 20 Jan 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/hugo-static-website-azure-functions-and-return-values/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I am using an Azure Function as backend for processing forms submissions from a Hugo static website, to process a simple contact form.&lt;/p&gt;
&lt;p&gt;I wanted to add reCAPTCHA support, as the site was generating too many spam emails. I also wanted to show a different confirmation pages depending on whether the reCAPTCHA check passed or failed&lt;/p&gt;
&lt;p&gt;There a good few posts about using an Azure Function as backend for a static web site form. But what I could not find was how to handle the return value from the Azure Function.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>I am using an Azure Function as backend for processing forms submissions from a Hugo static website, to process a simple contact form.</p>
<p>I wanted to add reCAPTCHA support, as the site was generating too many spam emails. I also wanted to show a different confirmation pages depending on whether the reCAPTCHA check passed or failed</p>
<p>There a good few posts about using an Azure Function as backend for a static web site form. But what I could not find was how to handle the return value from the Azure Function.</p>
<p>So as I have a solution, I thought a blog post would be a good idea to share it.</p>
<h2 id="the-solution">The solution</h2>
<p>On my contact form Hugo layout page, I have a hidden iframe, this is used as the target for the HTML form i.e. where the Azure Function posts back too. The Azure function returns a simple string, either &ldquo;OK&rdquo; or an error message depending on the outcome of the processing. The hidden target iframe has an onload event handler that checks the return value and redirects the user to a different page depending on the outcome.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span><span class="p">&gt;</span><span class="kd">var</span> <span class="nx">submitted</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">iframe</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;hidden_iframe&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;hidden_iframe&#34;</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;display:none;&#34;</span> <span class="na">onload</span><span class="o">=</span><span class="s">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">    if(submitted) {
</span></span></span><span class="line"><span class="cl"><span class="s">        const res = document.getElementById( &#39;hidden_iframe&#39; ).contentWindow.document.body.innerText +&#39;]&#39;;
</span></span></span><span class="line"><span class="cl"><span class="s">        if (res.includes(&#39;OK&#39;)) {
</span></span></span><span class="line"><span class="cl"><span class="s">          window.location=&#39;/confirmation/enquiry&#39;;
</span></span></span><span class="line"><span class="cl"><span class="s">        } else {
</span></span></span><span class="line"><span class="cl"><span class="s">          window.location=&#39;/confirmation/error&#39;;
</span></span></span><span class="line"><span class="cl"><span class="s">        }
</span></span></span><span class="line"><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="cl"><span class="s">    &#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">iframe</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;contact&#34;</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;/api/GenericFormsHandler&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;POST&#34;</span> <span class="na">target</span><span class="o">=</span><span class="s">&#34;hidden_iframe&#34;</span> <span class="na">onsubmit</span><span class="o">=</span><span class="s">&#34;submitted=true;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;g-recaptcha-response&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;g-recaptcha-response&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://www.google.com/recaptcha/api.js?render={{.Site.Data.reCAPCHA.key}}&amp;hl=en&#34;</span>  <span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">grecaptcha</span> <span class="o">!==</span> <span class="s1">&#39;undefined&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">grecaptcha</span><span class="p">.</span><span class="nx">ready</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nx">grecaptcha</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="s1">&#39;{{.Site.Data.reCAPCHA.key}}&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="s1">&#39;action&#39;</span><span class="o">:</span> <span class="s1">&#39;submit&#39;</span> <span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;g-recaptcha-response&#39;</span><span class="p">).</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">token</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">});</span>
</span></span><span class="line"><span class="cl">            <span class="p">});</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- all my input fields --&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;submitButton&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;Submit&#34;</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>My <a href="https://learn.microsoft.com/en-us/azure/static-web-apps/add-api?tabs=vanilla-javascript">Azure Static WebSite is configured to contain a managed Azure Function</a> with an HTTP trigger &lsquo;/api/GenericFormsHandler&rsquo; to handle the forms processing.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">AzureFunction</span><span class="p">,</span> <span class="nx">Context</span><span class="p">,</span> <span class="nx">HttpRequest</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@azure/functions&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">fetch</span> <span class="kr">from</span> <span class="s2">&#34;node-fetch&#34;</span><span class="p">;</span> <span class="c1">// needs to be installed with npm i node-fetch@2.6.1 
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">httpTrigger</span>: <span class="kt">AzureFunction</span> <span class="o">=</span> <span class="kr">async</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">context</span>: <span class="kt">Context</span><span class="p">,</span> <span class="nx">req</span>: <span class="kt">HttpRequest</span><span class="p">)</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">void</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;An HTTP POST trigger function to processed enquiry forms&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="kt">object</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">returnValue</span> <span class="o">=</span> <span class="s2">&#34;OK&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">object</span><span class="p">[</span><span class="s2">&#34;events&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">status</span> <span class="o">=</span> <span class="mi">200</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// a very basic HTML form to object parser
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s1">&#39;&amp;&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">field</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">pair</span> <span class="o">=</span> <span class="nx">field</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">&#34;=&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kt">object</span><span class="p">[</span><span class="nx">pair</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span> <span class="o">=</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">pair</span><span class="p">[</span><span class="mi">1</span><span class="p">]).</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/\+/g</span><span class="p">,</span> <span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Validating recaptcha token&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// the secret key is stored in the Azure Function App settings
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">postData</span> <span class="o">=</span> <span class="sb">`secret=</span><span class="si">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">RECAPTCHA_SECRETKEY</span><span class="si">}</span><span class="sb">&amp;response=</span><span class="si">${</span><span class="kt">object</span><span class="p">[</span><span class="s2">&#34;g-recaptcha-response&#34;</span><span class="p">]</span><span class="si">}</span><span class="sb">`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`reCAPTCHA request payload: </span><span class="si">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">postData</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// recaptcha validation only accepts POST requests using &#39;application/x-www-form-urlencoded&#39; content type
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">&#34;https://www.google.com/recaptcha/api/siteverify&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;POST&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">body</span>: <span class="kt">postData</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;Content-Type&#39;</span><span class="o">:</span> <span class="s1">&#39;application/x-www-form-urlencoded&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s1">&#39;Content-Length&#39;</span><span class="o">:</span> <span class="sb">`</span><span class="si">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">postData</span><span class="p">).</span><span class="nx">length</span><span class="si">}</span><span class="sb">`</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Error calling reCAPTCHA&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nx">returnValue</span> <span class="o">=</span> <span class="s2">&#34;Error&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">&gt;=</span> <span class="mi">400</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;HTTP Error from to reCAPTCHA: &#39;</span> <span class="o">+</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">+</span> <span class="s1">&#39; - &#39;</span> <span class="o">+</span> <span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nx">returnValue</span> <span class="o">=</span> <span class="s2">&#34;HTTPError&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Successful call to reCAPTCHA&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`reCAPTCHA response: </span><span class="si">${</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// if the score is less than the minimum score (App settings) then we don&#39;t process the form
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">success</span> <span class="o">&amp;&amp;</span> <span class="nx">data</span><span class="p">.</span><span class="nx">score</span> <span class="o">&gt;=</span> <span class="nb">parseFloat</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">RECAPTCHA_MINSCORE</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Sending email as reCAPTCHA detected a human&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">            <span class="kr">const</span> <span class="nx">sgMail</span> <span class="o">=</span> <span class="kr">require</span><span class="p">(</span><span class="s1">&#39;@sendgrid/mail&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="nx">sgMail</span><span class="p">.</span><span class="nx">setApiKey</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SENDGRID_API_KEY</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="kr">const</span> <span class="nx">msg</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="c1">// App Settings use for the from and to addresses
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>                <span class="nx">to</span>: <span class="kt">process.env.ENQUIRY_TOADDRESS</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                <span class="kr">from</span><span class="o">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">ENQUIRY_FROMADDRESS</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                <span class="nx">html</span><span class="o">:</span> <span class="c1">// add in the content generate from th form content
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="nx">sgMail</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Send Email returned  </span><span class="si">${</span><span class="nx">response</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">statusCode</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">response</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">returnValue</span> <span class="o">=</span> <span class="s2">&#34;OK&#34;</span>
</span></span><span class="line"><span class="cl">                <span class="p">})</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`ERROR sending email </span><span class="si">${</span><span class="nx">error</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">returnValue</span> <span class="o">=</span> <span class="nx">error</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">})</span>
</span></span><span class="line"><span class="cl">		
</span></span><span class="line"><span class="cl">		<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">           <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Not sending email as reCAPTCHA detected a bot&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="nx">returnValue</span> <span class="o">=</span> <span class="s2">&#34;reCAPTCHA Error&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">context</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Setting return value as </span><span class="si">${</span><span class="nx">returnValue</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">context</span><span class="p">.</span><span class="nx">res</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">body</span>: <span class="kt">returnValue</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">httpTrigger</span><span class="p">;</span>
</span></span></code></pre></div><h2 id="important">Important</h2>
<p>The key thing to note with this solution is that you get the Azure Function to write it&rsquo;s return value into the hidden iFrame on the calling page.</p>
<p>The only reason that this iFrame content (the return value) can read (using JavaScript on the form page) is because the Hugo static pages and managed Azure Function are in the same domain. So there are no cross site scripting issues blocking the reading of the iFrame contents e.g. your get no console error messages in the form:</p>
<blockquote>
<p>SecurityError: Blocked a frame with origin &ldquo;<a href="http://www.example.com">http://www.example.com</a>&rdquo; from accessing a cross-origin frame.</p></blockquote>
<p>Hence, the logic on the <code>onload</code> function can pick the correct confirmation page based on the value returned by the Azure Function.</p>
<h2 id="conclusion">Conclusion</h2>
<p>So, not the most elegant solution, but it works.</p>
<p>Hope this post save some other people some time</p>
]]></content:encoded>
    </item>
    <item>
      <title>Could not find assembly deploying a dotnet 6 console app</title>
      <link>https://blog.richardfennell.net/posts/could-not-find-assembly-deploying-dotnet6-app/</link>
      <pubDate>Fri, 13 Jan 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/could-not-find-assembly-deploying-dotnet6-app/</guid>
      <description>&lt;h2 id=&#34;problem&#34;&gt;Problem&lt;/h2&gt;
&lt;p&gt;After deploying a dotnet 6 console app to a production server, I got the following error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Exception: [Could not load file or assembly &amp;lsquo;System.Data.Odbc, Version=6.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51&amp;rsquo;. The system cannot find the file specified.]&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The strange thing was the same EXE, built on a Microsoft hosted Azure DevOps build agent, was working on the test server and was deployed using the same Azure DevOps Pipeline to both systems.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="problem">Problem</h2>
<p>After deploying a dotnet 6 console app to a production server, I got the following error:</p>
<blockquote>
<p>Exception: [Could not load file or assembly &lsquo;System.Data.Odbc, Version=6.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51&rsquo;. The system cannot find the file specified.]</p></blockquote>
<p>The strange thing was the same EXE, built on a Microsoft hosted Azure DevOps build agent, was working on the test server and was deployed using the same Azure DevOps Pipeline to both systems.</p>
<p>I tried many things to work out what the problem was. My first though was <a href="https://learn.microsoft.com/en-us/dotnet/framework/tools/fuslogvw-exe-assembly-binding-log-viewerdot-trace">Fuslogvw</a>, but found out I should be using <a href="https://github.com/dotnet/diagnostics/blob/main/documentation/dotnet-trace-instructions.md">dotnet-trace</a>, but this showed nothing other than the assembly was not found i.e. no downstream dependencies were missing.</p>
<p>I then manually pulled the same deployment ZIP onto the server, unpacked it and ran the EXE from the command line, it worked - what was the difference?</p>
<h2 id="solution">Solution</h2>
<p>Eventually I spotted the problem, the <code>runtimes</code> folder was missing on the copy that was deployed via the pipeline.</p>
<p>My automated deployment look like this</p>
<p><img alt="Bad folder structure" loading="lazy" src="/images/rfennell/noruntime.png"></p>
<p>But my manual deploy had a <code>runtimes</code> folder with the platform specific folders in the <code>runtimes</code> folder</p>
<p><img alt="Good folder structure" loading="lazy" src="/images/rfennell/runtime.png"></p>
<p>Remember, the same deployment pipeline was deploying the same artifact to both the test and production servers, and I could see the folder structure was correct in the build agent staging folder (where the ZIp was expanded prior to the copy). So why the differences?</p>
<p>And it got stranger, I redeployed the package using the same pipelines (just a re-run stage, not a complete now run) and this time the <code>runtimes</code> folder was there, and the EXE worked!</p>
<h2 id="conclusion">Conclusion</h2>
<p>I am just going to put this one down to a glitch in the matrix, maybe related to debris in the target folder?</p>
<p>But the learning is, when you get missing assembly errors with a .NET 6 (or .NET Core) deployment make sure the runtime folder is present and contains the platform specific folders.</p>
<p>Also make sure your target folder is empty before copying in the new code to avoid any debris from previous deployments just to be on the safe side.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Duplicate Test DLLs with vstest.console.exe causes failures</title>
      <link>https://blog.richardfennell.net/posts/duplicate-test-dlls-with-vstest.console.exe-causes-failures/</link>
      <pubDate>Mon, 09 Jan 2023 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/duplicate-test-dlls-with-vstest.console.exe-causes-failures/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I recently did our regular &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/building-private-vsts-build-agents-using-the-microsoft-packer-based-agent-image-creation-model/?query=build%20agents&#34;&gt;update of our Azure DevOps Private build agents&lt;/a&gt;. It is rare we see problems when we do this, but this time one of our very regularly run builds started to fail when running unit tests.&lt;/p&gt;
&lt;p&gt;We had not changed the project source code, all the test ran locally in Visual Studio. We had not change the build pipeline YAML&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-YAML&#34; data-lang=&#34;YAML&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;VSTest@2&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;displayName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Unit Tests - Services&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;inputs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;testAssemblyVer2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            **\*.unittests.dll
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;            !**\obj\**&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;searchFolder&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;&amp;#39;$(System.DefaultWorkingDirectory)/src&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And from the pipeline logs, we could see that the CLI command being generated by the &lt;code&gt;VSTest@2&lt;/code&gt; task was also unchanged, finding 39 DLLs that matched the filter.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2>
<p>I recently did our regular <a href="https://blogs.blackmarble.co.uk/rfennell/building-private-vsts-build-agents-using-the-microsoft-packer-based-agent-image-creation-model/?query=build%20agents">update of our Azure DevOps Private build agents</a>. It is rare we see problems when we do this, but this time one of our very regularly run builds started to fail when running unit tests.</p>
<p>We had not changed the project source code, all the test ran locally in Visual Studio. We had not change the build pipeline YAML</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-YAML" data-lang="YAML"><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">VSTest@2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Unit Tests - Services&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">testAssemblyVer2</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            **\*.unittests.dll
</span></span></span><span class="line"><span class="cl"><span class="sd">            !**\obj\**</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">searchFolder</span><span class="p">:</span><span class="w"> </span><span class="l">&#39;$(System.DefaultWorkingDirectory)/src</span><span class="w">
</span></span></span></code></pre></div><p>And from the pipeline logs, we could see that the CLI command being generated by the <code>VSTest@2</code> task was also unchanged, finding 39 DLLs that matched the filter.</p>
<p>So the only change was the version of the Visual Studio tools installed on the build agent. Specifically <code>vstest.console.exe</code> had been updated from Version 17.3.2 (x64) to Version 17.4.1 (x64)</p>
<h2 id="solution">Solution</h2>
<p>On checking the list of DLLs containing tests to be run we saw that some unit test DLLs were duplicated. They were appearing in their expected folders, but also in other project folders e.g.</p>
<blockquote>
<p>vstest.console.exe
&ldquo;E:\Agent_work\3\s\src\BM.Services\BM.Service1.UnitTests\bin\Release\BM.Service1.UnitTests.dll&rdquo;
&ldquo;E:\Agent_work\3\s\src\BM.Services\BM.Service2.UnitTests\bin\Release\BM.Service2.UnitTests.dll&rdquo;
&ldquo;E:\Agent_work\3\s\src\BM.Services\BM.Service2.UnitTests\bin\Release\BM.Service1.UnitTests.dll&rdquo;
&hellip;</p></blockquote>
<p>This was due to reference entries in the <code>.csproj</code> files that should not have been there.</p>
<p>So, the fix was to just remove the incorrectly present references to the DLLs in the various <code>.csproj</code> files.</p>
<p>This is one of those problems that makes you ask &lsquo;why has this not been an issue before?&rsquo;, but at least it is fixed now.</p>
]]></content:encoded>
    </item>
    <item>
      <title>GitHub agent Node version stops Hugo site build</title>
      <link>https://blog.richardfennell.net/posts/agent-node-version-stops-hugo-site-build/</link>
      <pubDate>Fri, 16 Dec 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/agent-node-version-stops-hugo-site-build/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I have &lt;a href=&#34;https://blogs.blackmarble.co.uk/search/?query=hugo&amp;amp;scope=rfennell&#34;&gt;blogged previously&lt;/a&gt; about moving various web sites over to become Hugo Static Sites.&lt;/p&gt;
&lt;p&gt;Recently one of my site&amp;rsquo;s, one using the &lt;a href=&#34;https://tailwindcss.com/&#34;&gt;tailwindscss module&lt;/a&gt;, GitHub Build and Deployment Workflow started failing with the following error:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Start building sites … 
hugo v0.108.0-a0d64a46e36dd2f503bfd5ba1a5807b900df231d+extended linux/amd64 BuildDate=2022-12-06T13:37:56Z VendorInfo=gohugoio
Error: Error building site: POSTCSS: failed to transform &amp;#34;css/style.css&amp;#34; (text/css): node:internal/errors:478
    ErrorCaptureStackTrace(err);
    ^

SystemError [ERR_SYSTEM_ERROR]: A system error occurred: uv_os_homedir returned ENOENT (no such file or directory)
    at Object.&amp;lt;anonymous&amp;gt; (/opt/nodejs/16.18.0/lib/node_modules/npm/node_modules/clean-stack/index.js:6:61)
    at Module._compile (node:internal/modules/cjs/loader:1155:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10)
    at Module.load (node:internal/modules/cjs/loader:1033:32)
    at Function.Module._load (node:internal/modules/cjs/loader:868:12)
    at Module.require (node:internal/modules/cjs/loader:1057:19)
    at require (node:internal/modules/cjs/helpers:103:18)
    at Object.&amp;lt;anonymous&amp;gt; (/opt/nodejs/16.18.0/lib/node_modules/npm/node_modules/aggregate-error/index.js:3:20)
    at Module._compile (node:internal/modules/cjs/loader:1155:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10) {
  code: &amp;#39;ERR_SYSTEM_ERROR&amp;#39;,
  info: {
    errno: -2,
    code: &amp;#39;ENOENT&amp;#39;,
    message: &amp;#39;no such file or directory&amp;#39;,
    syscall: &amp;#39;uv_os_homedir&amp;#39;
  },
  errno: [Getter/Setter],
  syscall: [Getter/Setter]
}
Total in 1653 ms
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;solution&#34;&gt;Solution&lt;/h2&gt;
&lt;p&gt;The problem it turned out was that the default version of Node used by the GitHub Actions runner had changed from Node 14 to Node 16.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2>
<p>I have <a href="https://blogs.blackmarble.co.uk/search/?query=hugo&amp;scope=rfennell">blogged previously</a> about moving various web sites over to become Hugo Static Sites.</p>
<p>Recently one of my site&rsquo;s, one using the <a href="https://tailwindcss.com/">tailwindscss module</a>, GitHub Build and Deployment Workflow started failing with the following error:</p>
<pre tabindex="0"><code>Start building sites … 
hugo v0.108.0-a0d64a46e36dd2f503bfd5ba1a5807b900df231d+extended linux/amd64 BuildDate=2022-12-06T13:37:56Z VendorInfo=gohugoio
Error: Error building site: POSTCSS: failed to transform &#34;css/style.css&#34; (text/css): node:internal/errors:478
    ErrorCaptureStackTrace(err);
    ^

SystemError [ERR_SYSTEM_ERROR]: A system error occurred: uv_os_homedir returned ENOENT (no such file or directory)
    at Object.&lt;anonymous&gt; (/opt/nodejs/16.18.0/lib/node_modules/npm/node_modules/clean-stack/index.js:6:61)
    at Module._compile (node:internal/modules/cjs/loader:1155:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10)
    at Module.load (node:internal/modules/cjs/loader:1033:32)
    at Function.Module._load (node:internal/modules/cjs/loader:868:12)
    at Module.require (node:internal/modules/cjs/loader:1057:19)
    at require (node:internal/modules/cjs/helpers:103:18)
    at Object.&lt;anonymous&gt; (/opt/nodejs/16.18.0/lib/node_modules/npm/node_modules/aggregate-error/index.js:3:20)
    at Module._compile (node:internal/modules/cjs/loader:1155:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10) {
  code: &#39;ERR_SYSTEM_ERROR&#39;,
  info: {
    errno: -2,
    code: &#39;ENOENT&#39;,
    message: &#39;no such file or directory&#39;,
    syscall: &#39;uv_os_homedir&#39;
  },
  errno: [Getter/Setter],
  syscall: [Getter/Setter]
}
Total in 1653 ms
</code></pre><h2 id="solution">Solution</h2>
<p>The problem it turned out was that the default version of Node used by the GitHub Actions runner had changed from Node 14 to Node 16.</p>
<p>I needed to explicitly set the version of Node to use in the workflow file to Node 14.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build_and_deploy_job</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.event_name == &#39;push&#39; || (github.event_name == &#39;pull_request&#39; &amp;&amp; github.event.action != &#39;closed&#39;)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build and Deploy Job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HUGO_VERSION</span><span class="p">:</span><span class="w"> </span><span class="m">0.108.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">NODE_VERSION</span><span class="p">:</span><span class="w"> </span><span class="m">14</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="l">....</span><span class="w">
</span></span></span></code></pre></div><p>Once this was set the build worked as expected.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Updating my Azure DevOps Pipeline Task to Use the Node16 Runner</title>
      <link>https://blog.richardfennell.net/posts/updating-my-azure-devops-tasks-to-node16/</link>
      <pubDate>Tue, 13 Dec 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/updating-my-azure-devops-tasks-to-node16/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;It is easy to create an Open Source Project and leave it to gather technical debt as the libraries it depends upon are updated.&lt;/p&gt;
&lt;p&gt;I have tried to keep on top of updating all &lt;a href=&#34;https://marketplace.visualstudio.com/search?term=fennell&amp;amp;target=AzureDevOps&amp;amp;category=All%20categories&amp;amp;sortBy=Relevance&#34;&gt;my Azure DevOps Pipeline Extensions&lt;/a&gt;, and I have to say &lt;a href=&#34;https://docs.github.com/en/code-security/dependabot/working-with-dependabot&#34;&gt;Dependabot&lt;/a&gt; has certainly helped, but I have not been as diligent as I might have been.&lt;/p&gt;
&lt;p&gt;So, as a Christmas project, I took the chance to start to do a major update of all my extensions. To make sure they used the newer Node16 execution runner (as per &lt;a href=&#34;https://github.com/microsoft/azure-pipelines-tasks/blob/master/docs/migrateNode16.md&#34;&gt;the document update process&lt;/a&gt;) and to update all the NPM packages used.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>It is easy to create an Open Source Project and leave it to gather technical debt as the libraries it depends upon are updated.</p>
<p>I have tried to keep on top of updating all <a href="https://marketplace.visualstudio.com/search?term=fennell&amp;target=AzureDevOps&amp;category=All%20categories&amp;sortBy=Relevance">my Azure DevOps Pipeline Extensions</a>, and I have to say <a href="https://docs.github.com/en/code-security/dependabot/working-with-dependabot">Dependabot</a> has certainly helped, but I have not been as diligent as I might have been.</p>
<p>So, as a Christmas project, I took the chance to start to do a major update of all my extensions. To make sure they used the newer Node16 execution runner (as per <a href="https://github.com/microsoft/azure-pipelines-tasks/blob/master/docs/migrateNode16.md">the document update process</a>) and to update all the NPM packages used.</p>
<h2 id="steps-part1">Steps (Part1)</h2>
<ul>
<li>In each <code>task.json</code> file
<ul>
<li>Update the minimum runner version</li>
<li>Add the Node16 based runner (assuming the task is Node based as most of mine are, I do have one or two PowerShell based ones)</li>
</ul>
</li>
<li>Update all the NPM packages. I used <a href="https://www.npmjs.com/package/npm-check-updates">npm-check-updates</a> to do this.</li>
<li>I then checked that task still built locally (the TypeScript linting and transpilation)</li>
<li>I then ran my mocha/chai based test locally, and this is where I hit problems.</li>
</ul>
<h2 id="problems-with-mocha-and-typescript">Problems with Mocha and TypeScript</h2>
<p>The error I got was in the form</p>
<blockquote>
<p>TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension &ldquo;.ts&rdquo; for D:\a\1\s\Extensions\WikiPDFExport\WikiPDFExportTask\test\foldercreation.test.ts</p></blockquote>
<p>Seems this is a <a href="https://github.com/mochajs/mocha/issues/4726">known issue</a>. As suggested, I tried swapping from the  <code>&quot;require&quot;: &quot;ts-node/register&quot;</code> to <code>&quot;loader&quot;: &quot;ts-node/esm&quot;</code> in the <code>.mocharc.json</code> file, this caused the error to changed to</p>
<blockquote>
<p>mocha ./test/<em>.test.ts
(node:31412) ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time (Use <code>node --trace-warnings ...</code> to show where the warning was created)
Warning: Cannot find any files matching pattern &ldquo;extensions/**/test/</em>.ts&rdquo;</p>
<p>TSError: ⨯ Unable to compile TypeScript:
test/foldercreation.test.ts:12:23 - error TS2695: Left side of comma operator is unused and has no side effects.
12         var actual = (0, GitWikiFunctions_1.GetWorkingFolder)(&quot;.\&quot;, &ldquo;testdata\subfolder\1\file.md&rdquo;, agentSpecific_1.logInfo);</p></blockquote>
<p>I battled with this for a while, but in the end gave up and just set my test macro to do the transpile first and then running mocha against the .js files, as opposed to letting mocha do all the steps itself.</p>
<p>So, swapping from</p>
<pre tabindex="0"><code>&#34;test-no-logger&#34;: &#34;mocha -r ts-node/register ./test/*.test.ts &#34;
</code></pre><p>to</p>
<pre tabindex="0"><code>&#34;transpile&#34;: &#34;tsc -p ./&#34;,
&#34;test-no-logger&#34;: &#34;npm run transpile &amp;&amp; mocha ./dist/test/*.test.js &#34;
</code></pre><p>I could now run my tests locally, but some were failing. This turned out to be due to using the older <code>del</code> NPM module. I swapped to <code>fs-extra</code> and all was good.</p>
<h2 id="steps-part2">Steps (Part2)</h2>
<p>When it all builds and tests locally you can continue</p>
<ul>
<li>As this is a major change and to give people the choice as to whether to update, I updated the Major version and zero&rsquo;d the Minor version numbers in the CI/CD pipeline that publishes the task (for my extensions I chose to manually manage the major versions, increment the minor version for each public release, and use the patch as build number)</li>
<li>Create PR and let the CI/CD pipeline deploy a private version of the extension to the Azure DevOps Marketplace</li>
<li>I can then increment the task version number in the pipeline YAML files so the new version is tested (you can&rsquo;t set this until you have deployed the incremented extension to the marketplace as the YAML validation will fail)</li>
</ul>
<p>And that should be it&hellip;</p>
<h2 id="summary">Summary</h2>
<p>This all seems a lot of work, but is essential if my OSS extensions are to trustworthy and usable. I have a few more to do, but I am hoping to get them all done before the end of the year.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fixing my Logitech Spotlight Presentation Remote that would not switch on</title>
      <link>https://blog.richardfennell.net/posts/fixing-my-logitech-spotlight/</link>
      <pubDate>Mon, 14 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fixing-my-logitech-spotlight/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I am getting back out in front of audiences again as opposed to doing Teams/Zoom events. So I dug out my trusty Logitech Spotlight presentation remote from the bag where it had been sitting for well over a year. However, there was a problem, it would not pair with my re-built Windows 11 PC. It could not even switch on.&lt;/p&gt;
&lt;p&gt;A quick search showed I was not alone in having this problem, &lt;a href=&#34;https://support.logi.com/hc/en-us/community/posts/360049560234-Spotlight-not-charging&#34;&gt;the Logitech forums showed a lot of angry people&lt;/a&gt;. The summary was, if left alone for a few months a Spotlight goes so flat it cannot be charged, and Logitech don&amp;rsquo;t have a fix, or seem to care. There were a lot of comments about very expensive paper weights.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-problem">The Problem</h2>
<p>I am getting back out in front of audiences again as opposed to doing Teams/Zoom events. So I dug out my trusty Logitech Spotlight presentation remote from the bag where it had been sitting for well over a year. However, there was a problem, it would not pair with my re-built Windows 11 PC. It could not even switch on.</p>
<p>A quick search showed I was not alone in having this problem, <a href="https://support.logi.com/hc/en-us/community/posts/360049560234-Spotlight-not-charging">the Logitech forums showed a lot of angry people</a>. The summary was, if left alone for a few months a Spotlight goes so flat it cannot be charged, and Logitech don&rsquo;t have a fix, or seem to care. There were a lot of comments about very expensive paper weights.</p>
<h2 id="the-fix">The Fix</h2>
<p>The solution, found a in thread comment on the Logitech Forum, was this very useful video from 2ai.</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/KuDt2un6zvI?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>The basic summary is to charge the battery via an external source to kick start the Spotlight. You can see a photo of the setup I used here.</p>
<p><img alt="My Spotlight fix setup" loading="lazy" src="/images/rfennell/spotlight-fix.jpg"></p>
<p>A few comments on the process:</p>
<ul>
<li>Getting the back off the Spotlight remote is a pain, even using my <a href="https://www.ifixit.com/Store/Tools/Pro-Tech-Toolkit/IF145-307">iFixit tools</a> I added a few scratches to the case.</li>
<li>As the video said, the soldering is delicate, you need to strip a way the cellophane to expose the solder points.</li>
<li>The video was not clear on the polarity or voltage of the battery/PSU.
<ul>
<li>The wire from the centre terminal on the Spotlight battery was connected to the +ve terminal on my PSU</li>
<li>The wire from the outside terminal on the Spotlight battery was connected to the -ve terminal on my PSU</li>
<li>I used a variable voltage power supply. I switched it on at its minimum of 3V and slowly increased the voltage until the Spotlight leapt into life at about 4V. This took only a few seconds.</li>
</ul>
</li>
<li>As soon as the Spotlight came to life, I plugged in a USB-C cable and removed my externally connected the PSU connections. Leaving the Spotlight to fully charge via USB.</li>
</ul>
<p>So a slightly fiddly fix, but it worked. Much better than the alternative of buying a new remote and sending the old one to e-waste/landfill.</p>
<h2 id="updated--16-nov-22---i-spoke-to-soon">Updated  16 Nov 22 - I spoke to soon</h2>
<p>It seems the fix was only temporary. The battery in the Spotlight is not holding charge, so I have a remote that works as long as I have it plugging in via a USB-C charging cable (or I suppose an external battery pack). So, not the most remote of remote presenter devices!</p>
<p>So I did more digging and found this <a href="https://www.ifixit.com/Guide/Logitech&#43;Spotlight&#43;Battery&#43;Replacement/164429">battery replacement guide on iFixit</a>. So, I have ordered a suitable battery and will update this post when it arrives and I have done the replacement.</p>
<h2 id="updated--19-nov-22---success-at-last">Updated  19 Nov 22 - Success at last</h2>
<p><img alt="Replacement battery for my Spotlight fix setup" loading="lazy" src="/images/rfennell/spotlight-fix2.jpg"></p>
<p>I have fitted the replacement <a href="https://www.ebay.co.uk/itm/234519858817">90mAh 3.7V Lithium Polymer Li-Po li ion Rechargeable Battery</a> and the Spotlight is now working as it should. I did have not had to charge it, via the USB-C cable, but once done the remote works as it should.</p>
<p>It will be interesting to see how long the battery lasts, but even if it has to be charged more often, it is still a lot better than having a dead remote.</p>
<p>So if you have this issue, I would recommend a battery swap. It is not that hard to do, and the battery is not expensive. Much better than a $150 paper weight.</p>
<h2 id="updated-27-sept-2023---i-let-it-go-flat-again">Updated 27 Sept 2023 - I let it go flat again!</h2>
<p>I left my Logitech Spotlight in my bag and it went flat again. So I was back to square one, it would not charge.</p>
<p>I first tried to &lsquo;jump start it&rsquo; again, putting 4V from an external PSU across the battery terminals while it was also connected to USB power to charge the battery.</p>
<p>Initially it appeared to work, but as soon as I removed the external USB power it died again.</p>
<p>Next, I unsoldered the battery and tested it, it seemed OK giving around 4V.</p>
<p>So I reconnected it, plugged in USB power, this time overnight, then tested it again. This time it worked.</p>
<p>So either it was a poor battery connection, or the battery just needed a much longer charge as it was so flat.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using Azure DevOps Migration Tools Again</title>
      <link>https://blog.richardfennell.net/posts/time-for-azure-devops-migration-tools-again/</link>
      <pubDate>Fri, 11 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/time-for-azure-devops-migration-tools-again/</guid>
      <description>&lt;p&gt;I have recently been working with a client who needed to move Azure DevOps Work Items between Team Projects on different Azure DevOps instances. The &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/options-migrating-tfs-to-vsts/&#34;&gt;only realistic choice&lt;/a&gt; was to use the free open source &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=nkdagility.vsts-sync-migration&#34;&gt;Azure DevOps Migration Tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have used these tools before, but it was a while ago, and as it is under active development, I had to relearn a few things. It is fair to say that the learning curve for this tool is steep, but as the documentation does not hide this&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have recently been working with a client who needed to move Azure DevOps Work Items between Team Projects on different Azure DevOps instances. The <a href="https://blogs.blackmarble.co.uk/rfennell/options-migrating-tfs-to-vsts/">only realistic choice</a> was to use the free open source <a href="https://marketplace.visualstudio.com/items?itemName=nkdagility.vsts-sync-migration">Azure DevOps Migration Tools</a>.</p>
<p>I have used these tools before, but it was a while ago, and as it is under active development, I had to relearn a few things. It is fair to say that the learning curve for this tool is steep, but as the documentation does not hide this</p>
<blockquote>
<p>WARNING: This tool is not designed for a novice. This tool was developed to support the scenarios below, and the edge cases that have been encountered by the 30+ contributors from around the Azure DevOps community. You should be comfortable with the TFS/Azure DevOps object model, as well as debugging code in Visual Studio</p></blockquote>
<p>So where did I trip up this time?</p>
<h2 id="but-the-documentation">But the documentation!</h2>
<p>The documentation is OK, but can be confusing at the start as page links loop back on themselves. Adding to confusion is that there are details for older legacy V1 processors, current V1 processors and new V2 ones, but the reality is you are probably using only a small subset of the ones listed. Probably just current V1 ones for most use cases.</p>
<p>In my case I could ignore all bar the <code>WorkItemMigrationConfig</code>processor and the three &lsquo;beta&rsquo; <code>Test*MigrationConfig</code> processors, and I suspect this is true for many people. This might not appear the case on a first read of the documentation.</p>
<p>Today, the <code>WorkItemMigrationConfig</code> processor is the one that does the bulk of the work, doing the work previously done by a set of legacy processors e.g creating Iteration and Area nodes.</p>
<h2 id="recreating-configuration-files">Recreating Configuration Files</h2>
<p>Always recreate your configuration file when you update the version of the migration tools you are using. The whole schema could have changed and some option settings certainly will have.</p>
<p>The documentation does tell you to do this, don&rsquo;t be tempted to just update the version number in the config file, it usually does not work as you miss a required new setting.
It is invariably quicker to re-add in your source and target details, rather than work out what other flag has changed.</p>
<h2 id="watch-our-for-case-sensitivity">Watch our for case sensitivity</h2>
<p>I found that the current version, V12, is much more case sensitive than previous versions. I initially had my target Team Project name in lower case, and the tools failed in the strangest way.</p>
<p>A connection was successfully made to the team projects, but when it tried to create the missing iteration or area paths it failed with message about being unable to create nodes i.e.</p>
<p><code>NewNode is not anchored in the target project, it cannot be created</code></p>
<p>A search of <a href="https://github.com/nkdAgility/azure-devops-migration-tools/issues">GitHub Issues</a>, <a href="https://github.com/nkdAgility/azure-devops-migration-tools/discussions">GitHub Discussions</a> and <a href="https://stackoverflow.com/questions/tagged/azure-devops-migration-tools">Stack overflow thread</a> for this error suggested problem with the configuration values for the <code>NodeBasePaths</code> settings. However, after a bit of interactive debugging, I found this was not the root cause. In fact, as I was doing a simple copy migration all tht was needed was an empty array for the <code>NodeBasePaths</code>, as I had first thought.</p>
<p>The debugging showed that the problem was the case of the target project name. The mismatch meant a regex expression was failing to match the built in root Area and Iteration nodes. Hence the error.</p>
<p>So, top tip, copy and paste in URLs and Team Project names to avoid stupid typos</p>
<h2 id="how-to-increase-the-chances-of-a-successful-migration">How to increase the chances of a successful migration</h2>
<p>I always start by migrating over a single work item of a given type into a test target project. When this works, I try a single work item of another type. I repeat this process until I have migrated all the work item types in use.</p>
<p>I will try to selectively pick sample work items so there are relationships between them, thus ensuring the linking is working, and ones with attachments, to ensure those are migrated correctly too.</p>
<p>Once I know all the work item types can be migrated, then and only then do i try to run a migration for all the work items in the source project.</p>
<p>It is far better to spend a few minutes making sure all the work item type field and states are mapped correctly, rather than finding a few hours into a long migration run.</p>
<p>The way I do this filtering is to edit the <code>WIQLQueryBit</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;WIQLQueryBit&#34;</span><span class="err">:</span> <span class="s2">&#34;AND  [Microsoft.VSTS.Common.ClosedDate] = &#39;&#39; AND [System.WorkItemType] NOT IN (&#39;Test Suite&#39;, &#39;Test Plan&#39;,&#39;Shared Steps&#39;,&#39;Shared Parameter&#39;,&#39;Feedback Request&#39;) AND [System.Id] = &#39;1234&#39;&#34;</span><span class="err">,</span>
</span></span></code></pre></div><blockquote>
<p><strong>Note:</strong> The configuration file does contain a <code>&quot;WorkItemIDs&quot;:[]</code> setting that the documentation says is &ldquo;A list of work items to import&rdquo;. I had assumed this would filter the work items returned by the WIQL query, it does not. After looking at the code it does not seem to be used at present in the <code>WorkItemMigrationConfig</code> processor.</p></blockquote>
<p>A key advantage of the WIQL approach is by filtering the number of work items returned to a single one, the migration tools will start much more quickly. There is a &rsquo;tax&rsquo; on every run of the migration tools where it has to do analysis of the source and target projects to work out what needs to be migrated. If it has to check hundreds or thousands of work items this will take many minutes. However, if it only has to check one, it will be done in, from my experience, around 10 seconds</p>
<h2 id="summary">Summary</h2>
<p>Using this tool is always an adventure, but it does what it says it will when you have it configured correctly.</p>
<p>And if you do have problems, the debugging logs are detailed, and there is active support on <a href="https://github.com/nkdAgility/azure-devops-migration-tools/issues">GitHub Issues</a> and <a href="https://github.com/nkdAgility/azure-devops-migration-tools/discussions">GitHub Discussions</a>.</p>
<p>If you do have problems, don&rsquo;t be afraid of running the tool using Visual Studio. This is a great way to debug the flow and see what is going on. If you do make changes remember this is an open source project and your bug fix and enhancement PRs will always be considered.</p>
<p>The bottom line is that you just have to accept that this form of migration will be a slow process, but once you have the basics of the configuration file correct the tool is reliable.</p>
]]></content:encoded>
    </item>
    <item>
      <title>GitVersion task fails on a cloned Azure DevOps YAML Pipeline</title>
      <link>https://blog.richardfennell.net/posts/gitversion-fails-on-a-cloned-yaml-build/</link>
      <pubDate>Tue, 08 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/gitversion-fails-on-a-cloned-yaml-build/</guid>
      <description>&lt;h2 id=&#34;problem&#34;&gt;Problem&lt;/h2&gt;
&lt;p&gt;I recently had a strange problem. I had an existing Azure DevOps YAML Pipeline that used the checkout task to do a deep Git fetch of a repo and it&amp;rsquo;s submodules. The reason for the deep fetch was that later in the pipeline we ran GitVersion and this needs the whole repo to be able to calculate the version.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;- &lt;span class=&#34;nt&#34;&gt;checkout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;persistCredentials&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;submodules&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;gitversion/setup@0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;displayName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Get current version of GitVersion&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;inputs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;versionSpec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;5.x&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;gitversion/execute@0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;displayName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Run GitVersion to generate SEMVER&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;inputs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;useConfigFile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;configFilePath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$(System.DefaultWorkingDirectory)/GitVersion.yml&amp;#39;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On this original pipeline this was all working as expected.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="problem">Problem</h2>
<p>I recently had a strange problem. I had an existing Azure DevOps YAML Pipeline that used the checkout task to do a deep Git fetch of a repo and it&rsquo;s submodules. The reason for the deep fetch was that later in the pipeline we ran GitVersion and this needs the whole repo to be able to calculate the version.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">checkout</span><span class="p">:</span><span class="w"> </span><span class="l">self</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">persistCredentials</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">submodules</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">gitversion/setup@0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Get current version of GitVersion&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">versionSpec</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;5.x&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">task</span><span class="p">:</span><span class="w"> </span><span class="l">gitversion/execute@0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">displayName</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Run GitVersion to generate SEMVER&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">useConfigFile</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">configFilePath</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;$(System.DefaultWorkingDirectory)/GitVersion.yml&#39;</span><span class="w">
</span></span></span></code></pre></div><p>On this original pipeline this was all working as expected.</p>
<p>However, when I created a second Azure DevOps pipeline that pointed to the same YAML file, expecting it to work without issues, it failed at the GitVersion task. It could not calculate the version.</p>
<p>On checking the pipeline logs, I could see that the git checkout had used a depth of 1 i.e. a shallow fetch, more efficient, but not providing all the branch and commit information GitVersion needs</p>
<h2 id="the-fix">The fix</h2>
<p>It seems that on this 2nd pipeline the UX based pipeline settings were set differently, overriding the YAML ones, so causing a shallow fetch to be used.</p>
<p><img alt="ScreenShot" loading="lazy" src="/images/rfennell/YMLSettingsScrrenShot.png"></p>
<p>When I switched off the UX &lsquo;set shallow fetch&rsquo; all worked as expected [accessed via Pipeline &gt; Edit &gt; Ellipsis menu &gt; Triggers &gt; YAML tab]</p>
<p>Confusing that a cloned pipeline should end up with different settings, especially when the setting you need to change is far from easily discoverable</p>
]]></content:encoded>
    </item>
    <item>
      <title>Migrating our &#34;Living the Dream&#34; DevOps demo to GitHub Enterprise</title>
      <link>https://blog.richardfennell.net/posts/migrating-living-the-dream-to-github/</link>
      <pubDate>Tue, 01 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/migrating-living-the-dream-to-github/</guid>
      <description>&lt;p&gt;At Black Marble, we have had a long standing Azure DevOps Team Project that we used for end-to-end demos of the principles of DevOps called &lt;a href=&#34;https://www.youtube.com/watch?v=TCmwR-HdvSk&amp;amp;index=1&amp;amp;list=PLiP6RW7A4433fa1t77ZU4aq0DM08brY5t&#34;&gt;Living the Dream&lt;/a&gt;. This used a legacy codebase, the old Microsoft Fabrikam demo, and showed that can be deployed using modern tools.&lt;/p&gt;
&lt;p&gt;As I had no similar demo for GitHub Enterprise, I thought it would be interesting to see how the migration process taking my Azure DevOps implementation over to GitHub would go. This is a good learning exercise as it is the type of problem that many of our enterprise clients will need to do if changing DevOps platform. My key aim was to do the minimum to get the CI/CD process moved from Azure Pipelines to GitHub Action&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>At Black Marble, we have had a long standing Azure DevOps Team Project that we used for end-to-end demos of the principles of DevOps called <a href="https://www.youtube.com/watch?v=TCmwR-HdvSk&amp;index=1&amp;list=PLiP6RW7A4433fa1t77ZU4aq0DM08brY5t">Living the Dream</a>. This used a legacy codebase, the old Microsoft Fabrikam demo, and showed that can be deployed using modern tools.</p>
<p>As I had no similar demo for GitHub Enterprise, I thought it would be interesting to see how the migration process taking my Azure DevOps implementation over to GitHub would go. This is a good learning exercise as it is the type of problem that many of our enterprise clients will need to do if changing DevOps platform. My key aim was to do the minimum to get the CI/CD process moved from Azure Pipelines to GitHub Action</p>
<h2 id="moving-the-source">Moving the source</h2>
<p>This was easy, I just <a href="https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/importing-a-repository-with-github-importer">imported the Git repo from Azure DevOps</a></p>
<h2 id="build">Build</h2>
<p>The build for my solution was fairly easy, the project is a pair of Visual Studio solutions, one for the ARM code and the other for the Website code.</p>
<p>The key point was to make sure I passed in the correct parameters to make sure the web site was packaged up using WebDeploy</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">Build-Solution</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">windows-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Add MSBuild to PATH</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">microsoft/setup-msbuild@v1.0.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Restore NuGet packages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">working-directory</span><span class="p">:</span><span class="w"> </span><span class="l">${{env.GITHUB_WORKSPACE}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">nuget restore ${{env.SOLUTION_FILE_PATH}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build Solution</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">working-directory</span><span class="p">:</span><span class="w"> </span><span class="l">${{env.GITHUB_WORKSPACE}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Add additional options to the MSBuild command line here (like platform or verbosity level).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># See https://docs.microsoft.com/visualstudio/msbuild/msbuild-command-line-reference</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} /p:DeployOnBuild=true /p:PublishProfile=${{env.BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}}</span><span class="w">
</span></span></span></code></pre></div><h2 id="deployment">Deployment</h2>
<p>The majority of the work was required to get the solution deployed i.e.</p>
<ul>
<li>Creating all the required Azure resources using an ARM template</li>
<li>Deploying the website via MSDeploy.</li>
</ul>
<p>In Azure DevOps I had tasks to manage these steps, for GitHub actions, though some actions exist, I also had to write some scripts.</p>
<p>I ended up putting the Actions required for the ARM and Solution deployment in <a href="https://docs.github.com/en/actions/using-workflows/reusing-workflows">reusable workflows</a>, so I could call the steps at multiple locations on my workflows (for the test deployment and the production deployment) without repeating actions.</p>
<h3 id="arm">ARM</h3>
<p>The workflow for the ARM was as follows. Nothing that special, the key points to note are</p>
<ul>
<li>You have to use the <a href="https://github.com/marketplace/actions/azure-login">Azure Login action</a>, this in effect replaces the Azure Pipelines service connection.</li>
<li>I have to use a script to check the Azure resource group exists prior to the deployment. In Azure Pipelines the ARM task will create the resource group if not present, but this is not so with the GitHub Action</li>
<li>I inject all my ARM parameters as inline parameters (as opposed to a file), this was just to keep the same pattern as had been used on Azure DevOps</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Reuseable workflow to publish ARM to Azure resource group</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_call</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># all secrets inherited    </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">Integration-ARM</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">windows-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">${{ inputs.environment }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Download ARM Build Artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact@v3.0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">arm-download</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Artifact name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ARM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./ARM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Azure Login</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">Azure/login@v1.4.6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">creds</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AZURE_CREDENTIALS }}     </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Ensure Azure Resource Group is created</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        $rgexists = az group exists -n ${{ secrets.AzureResourceGroup}}
</span></span></span><span class="line"><span class="cl"><span class="sd">        if ($rgexists -eq &#39;false&#39;) {
</span></span></span><span class="line"><span class="cl"><span class="sd">          az group create --name ${{ secrets.AzureResourceGroup}} --location ${{ secrets.AZURELOCATION }}
</span></span></span><span class="line"><span class="cl"><span class="sd">          write-host &#34;Creating Azure Resource Group&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">pwsh </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy Azure Resource Manager (ARM) Template</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">Azure/arm-deploy@v1.0.8</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">scope</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;resourcegroup&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">subscriptionId</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AZURESUBSCRIPTION }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">region</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AZURELOCATION }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">resourceGroupName</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AzureResourceGroup}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;.\\ARM\\Templates\\WebSiteSQLDatabase.json&#39;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">deploymentMode</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Incremental&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">parameters</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;hostingPlanName=&#34;${{ secrets.HostingPlanName}}&#34; hostingPlanSku=&#34;${{ secrets.hostingPlanSku}}&#34; hostingPlanCapacity=&#34;${{ secrets.hostingPlanCapacity}}&#34; webSiteName=&#34;${{ secrets.Sitename}}&#34; sqlserverName=&#34;${{ secrets.sqlservername}}&#34; sqlServerAdminLogin=&#34;${{ secrets.SQLUser}}&#34; sqlServerAdminPassword=&#34;${{ secrets.SQLPassword}}&#34; databaseName=&#34;${{ secrets.databasename}}&#34; collation=&#34;SQL_Latin1_General_CP1_CI_AS&#34; edition=&#34;Standard&#34; maxSizeBytes=&#34;1073741824&#34; requestedServiceObjectiveName=&#34;S0&#34; appInsightsLocation=&#34;${{ secrets.AzureLocation}}&#34; VersionTag=&#34;1.2.3&#34;&#34; DeploymentDate=&#34;2022-10-28&#34;&#34; EnvironmentTag=&#34;tag&#34;&#39;</span><span class="w">
</span></span></span></code></pre></div><h3 id="solution">Solution</h3>
<p>The MSDeploy was more problematic. It is fair to say this is not a currently fashionable technology. Web deploy has very much moved to the &lsquo;copy a zip file&rsquo; approach. There was no GitHub Action available to run MSDeploy against Azure, so I had to work out the parameters and script it.</p>
<p>Again the key points to note</p>
<ul>
<li>I could not find a GitHub Action that would automatically update a configuration file replacing tokens found with values from environment variables and secrets. There a few that do part of the job, but not it all. This is something I might well write, but for now a script that replaces the tokens did the job - and yes I know it is probably better practice to set these values in the Azure WebApp directly, but as I said I was trying for a like for like replacement.</li>
<li>The MSDeploy relies on pulling down the publish profile then calling the MSDeploy EXE which is present on the GitHub Agent. This took a while to get right, but once it was done, it is reliable</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Reuseable workflow to publish web solution to Azure WebApp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_call</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># all secrets inherited    </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">Integration-ARM</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">windows-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">${{ inputs.environment }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Download Web Deploy Solution Build Artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact@v3.0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Artifact name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Webdeploy-Package&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./WebDeploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Replace tokens in configuration file</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        $file = &#34;.\WebDeploy\FabrikamFiber.Web.SetParameters.xml&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = Get-Content -Path $file
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__Sitename__&#34;, &#34;${{secrets.Sitename}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__LOCATION__&#34;, &#34;${{secrets.LOCATION}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__GENERATETESTDATA__&#34;, &#34;${{secrets.GENERATETESTDATA}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__sqlservername__&#34;, &#34;${{secrets.sqlservername}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__databasename__&#34;, &#34;${{secrets.databasename}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__SQLUser__&#34;, &#34;${{secrets.SQLUser}}&#34; 
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent = $filecontent -replace &#34;__SQLPassword__&#34;, &#34;${{secrets.SQLPassword}}&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">        $filecontent | Out-File $file
</span></span></span><span class="line"><span class="cl"><span class="sd">        cat $file          </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">pwsh</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Azure Login</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">Azure/login@v1.4.6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">creds</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AZURE_CREDENTIALS }}     </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;Deploy web site with MSDeploy&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">        $publishProfile = az webapp deployment list-publishing-profiles --resource-group ${{ secrets.AzureResourceGroup}} --name ${{ secrets.Sitename }} --query &#34;[?publishMethod==&#39;MSDeploy&#39;]&#34; --subscription &#34;${{ secrets.AZURESUBSCRIPTION}}&#34; | convertfrom-json
</span></span></span><span class="line"><span class="cl"><span class="sd">        $shortPath = (New-Object -ComObject Scripting.FileSystemObject).GetFolder(&#34;./WebDeploy&#34;).ShortPath  
</span></span></span><span class="line"><span class="cl"><span class="sd">        &amp; &#34;C:\Program Files\IIS\Microsoft Web Deploy V3\msdeploy.exe&#34; -verb:sync -source:package=&#34;$shortpath\FabrikamFiber.Web.zip&#34; -setParamFile:&#34;$shortpath\FabrikamFiber.Web.SetParameters.xml&#34; -dest:auto,ComputerName=&#34;https://$($publishProfile.msdeploySite).scm.azurewebsites.net/msdeploy.axd?site=$($publishProfile.msdeploySite)&#34;,UserName=$($publishProfile.userName),Password=$($publishProfile.userPWD),AuthType=&#39;Basic&#39; -verbose -debug -disableLink:AppPoolExtension -disableLink:ContentExtension -disableLink:CertificateExtension</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">pwsh</span><span class="w">
</span></span></span></code></pre></div><h2 id="summary">Summary</h2>
<p>So I now have the core of my &lsquo;Living the Dream&rsquo; demo on GitHub, the is more of course I can do, but it is a good start and has been a good learning experience.</p>
<p>This form of activity is something I would recommend to anyone trying to get their had around the intricacies of GitHub, or any technology new to them. You always learn more I think when trying to do your own project as opposed to just following a lab.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Ignite 2022 Azure DevOps &amp; GitHub Announcements - GitHub Advanced Security comes to Azure DevOps</title>
      <link>https://blog.richardfennell.net/posts/ignite-2022-azure-devops-and-github-announcements/</link>
      <pubDate>Wed, 12 Oct 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/ignite-2022-azure-devops-and-github-announcements/</guid>
      <description>&lt;p&gt;Today at &lt;a href=&#34;https://ignite.microsoft.com/&#34;&gt;Microsoft&amp;rsquo;s Ignite Conference&lt;/a&gt; there have been some very interesting announcements related to Azure DevOps and GitHub.&lt;/p&gt;
&lt;p&gt;In the recent past, I have seen confusion from our clients as to what is Microsoft&amp;rsquo;s recommended  DevOps solution, given they have both Azure DevOps and GitHub.&lt;/p&gt;
&lt;p&gt;It is true that Microsoft have said, and continue to say, that GitHub is the &amp;rsquo;north star&amp;rsquo; the long term destination for all users. However, that does not help clients today. Many of mine ask &amp;lsquo;but I am using Azure DevOps, but all Microsoft seem to talk about is GitHub, is Azure DevOps dead?&amp;rsquo;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Today at <a href="https://ignite.microsoft.com/">Microsoft&rsquo;s Ignite Conference</a> there have been some very interesting announcements related to Azure DevOps and GitHub.</p>
<p>In the recent past, I have seen confusion from our clients as to what is Microsoft&rsquo;s recommended  DevOps solution, given they have both Azure DevOps and GitHub.</p>
<p>It is true that Microsoft have said, and continue to say, that GitHub is the &rsquo;north star&rsquo; the long term destination for all users. However, that does not help clients today. Many of mine ask &lsquo;but I am using Azure DevOps, but all Microsoft seem to talk about is GitHub, is Azure DevOps dead?&rsquo;.</p>
<p>Well today&rsquo;s announcements in the conference session <a href="https://ignite.microsoft.com/en-US/sessions/8847d725-4863-4cea-961f-52e1df342709">&lsquo;Accelerate innovation with the world&rsquo;s most complete cloud developer platform&rsquo;</a> and the blog post <a href="https://devblogs.microsoft.com/devops/integrate-security-into-your-developer-workflow-with-github-advanced-security-for-azure-devops/">&lsquo;Integrate security into your developer workflow with GitHub Advanced Security for Azure DevOps&rsquo;</a> goes a long way to answering that question with a positive answer.</p>
<p>&lsquo;Cool features&rsquo; from GitHub Enterprise, specifically Code Advanced Security, are coming to Azure DevOps, along with other Azure DevOps specific investments. Thus greatly re-enforcing the &lsquo;better together&rsquo; story for Azure DevOps and GitHub, and that Azure DevOps has a future.</p>
<p>These changes could be taken two ways, depending on your position</p>
<ul>
<li>You could argue it makes it much clearer for both existing Azure DevOps and GitHub users to see their road-map, with both products sharing a common set of share services</li>
<li>Conversely you could argue it muddles the waters and slows the inevitable move to GitHub</li>
</ul>
<p>I suspect which position you take is dependent as to whether you are an Azure DevOps or GitHub Enterprise user.</p>
<p>If this is an area of interest, I will be talking more about these changes at Black Marble&rsquo;s next free webinar titled <a href="https://www.blackmarble.com/events/43">&lsquo;Which is best for me&hellip; GitHub, Azure DevOps, or Better Together?&rsquo;</a> on 26th October 2022.</p>
]]></content:encoded>
    </item>
    <item>
      <title>SonarCloud PR branch analysis when the main/trunk branch has not been analysed</title>
      <link>https://blog.richardfennell.net/posts/sonarcloud-pr-analysis-when-the-main-branch-has-not-been-analysed/</link>
      <pubDate>Sat, 01 Oct 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/sonarcloud-pr-analysis-when-the-main-branch-has-not-been-analysed/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://sonarcloud.io&#34;&gt;SonarCloud&lt;/a&gt; (and it&amp;rsquo;s on premise equivalent &lt;a href=&#34;https://www.sonarqube.org/&#34;&gt;SonarQube&lt;/a&gt;) understand the concept of Git branching and PRs (in various platforms, in my case Azure DevOps was the important one). This means you can &lt;a href=&#34;https://docs.sonarcloud.io/improving/pull-request-analysis/&#34;&gt;block the completion of a PR&lt;/a&gt; if the new code in the branch/PR does not meet the SonarCloud Quality Gate. A great way to stop the addition of technical debt.&lt;/p&gt;
&lt;p&gt;However, I recently found a problem when starting to use SonarCloud in an older codebase. You cannot do SonarCloud analysis of a child branch before the main/trunk has been analysed.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://sonarcloud.io">SonarCloud</a> (and it&rsquo;s on premise equivalent <a href="https://www.sonarqube.org/">SonarQube</a>) understand the concept of Git branching and PRs (in various platforms, in my case Azure DevOps was the important one). This means you can <a href="https://docs.sonarcloud.io/improving/pull-request-analysis/">block the completion of a PR</a> if the new code in the branch/PR does not meet the SonarCloud Quality Gate. A great way to stop the addition of technical debt.</p>
<p>However, I recently found a problem when starting to use SonarCloud in an older codebase. You cannot do SonarCloud analysis of a child branch before the main/trunk has been analysed.</p>
<p>This is most likely an issue because you are using Azure DevOps YAML Pipelines and must commit your revised pipeline, that triggers SonarCloud analysis, via a PR i.e. you cannot commit direct to main/trunk.</p>
<p>However, in my case I was using Azure DevOps Classic Builds and it was because the work I was doing was to move a codebase from an unsupported version of .NET to .NET 4.8. On the build agents available to me I had no means to build the old codebase, and I was not minded to provision agents just to do that job. So I looked for away to analyse the main/trunk branch to unblock my PR analysis.</p>
<p>Normally SonarCloud analysis is done as part of the automated build process, but there are <a href="https://docs.sonarcloud.io/advanced-setup/ci-based-analysis/sonarscanner-cli/">CLI tools</a> to manage it too. I decided to use these in the quickest dirtiest way I could think of</p>
<ol>
<li>I cloned my legacy repo (you never use the code in the repo, but it is needed so SonarCloud can detect the branch. You could possibly use an empty git repo as an alternative)</li>
<li>I switched to the main branch and created a folder off the roo</li>
<li>In this folder I created a new dotnet console app</li>
<li>From the root of the repo I ran the SonarCloud <a href="https://docs.sonarcloud.io/advanced-setup/ci-based-analysis/sonarscanner-for-net/">dotnet CLI Scanner</a>, passing in the details required to create a new SonarCloud project</li>
</ol>
<p><code>dotnet sonarscanner begin /k:&quot;AKEY&quot; /o:&quot;&lt;my sonarcloud org name&gt;&quot; /d:sonar.login=&quot;&lt;your  access token&gt;&quot; /d:sonar.host.url=&quot;https://sonarcloud.io&quot; /n:&quot;Project Name&quot; </code></p>
<ol start="5">
<li>I then built the dotnet project</li>
<li>Finally I completed the SonarCloud analysis</li>
</ol>
<p><code>dotnet sonarscanner end /d:sonar.login=&quot;&lt;your  access token&gt;&quot;</code></p>
<p>This created an analysis of the main branch with the single dotnet <code>program.cs</code> file</p>
<p>It is then possible to do the analysis of the child branches, with all the real code. What code is detected as &rsquo;new code&rsquo; in this branch by SonarCloud will be dependant on your <a href="https://docs.sonarcloud.io/improving/new-code-definition/">settings</a>. So even though the code on this first PR branch will have never been seen by SonarCloud, it might not be detected as new due to the file&rsquo;s last modified dates.</p>
<p>I realise I could have locally built the legacy code on my PC (removing steps 2 &amp; 3, and replacing step 5 with an MSBuild), but that would have required installing legacy frameworks I did not wish to do.</p>
<p>So a dirty hack, but got me out of a hole, and once the PR was completed SonarCloud showed all the code metrics I would expect.</p>
<h2 id="updated-3rd-oct-2022">Updated: 3rd Oct 2022</h2>
<p>Just for completeness, these are command lines to do a local MSBuild of an existing codebase using the CLI Scanner. A better options if you have a dev PC available to build the legacy code based but not suitable build agent (and don&rsquo;t want to make your PC a temporary build agent)</p>
<ol>
<li>Clone the legacy repo</li>
<li>From the root of the repo run the SonarCloud <a href="https://docs.sonarcloud.io/advanced-setup/ci-based-analysis/sonarscanner-for-net/">dotnet MSbuild CLI Scanner</a>, passing in the details required to create a new SonarCloud project</li>
</ol>
<p><code>sonarscanner.msbuild.exe begin /k:&quot;AKEY&quot; /o:&quot;&lt;my sonarcloud org name&gt;&quot; /d:sonar.login=&quot;&lt;your  access token&gt;&quot; /d:sonar.host.url=&quot;https://sonarcloud.io&quot; /n:&quot;Project Name&quot; </code></p>
<ol start="5">
<li>Built the legacy project(s) with MSBuild</li>
</ol>
<p><code>'C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild' .\MyProjectFolder\MyProject.csproj</code></p>
<ol start="6">
<li>Completed the SonarCloud analysis</li>
</ol>
<p><code>sonarscanner.msbuild.exe end /d:sonar.login=&quot;&lt;your  access token&gt;&quot;</code></p>
]]></content:encoded>
    </item>
    <item>
      <title>Showing OWASP Dependency Check results in SonarCloud</title>
      <link>https://blog.richardfennell.net/posts/linking-dependencycheck-and-sonarcloud/</link>
      <pubDate>Thu, 29 Sep 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/linking-dependencycheck-and-sonarcloud/</guid>
      <description>&lt;p&gt;The &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=dependency-check.dependencycheck&#34;&gt;OWASP Dependency Checker&lt;/a&gt; can be used to check for known vulnerabilities in a variety of eco-systems. This tool produces a HTML based report, but I wanted to expose the issues in &lt;a href=&#34;https://sonarcloud.io&#34;&gt;SonarCloud&lt;/a&gt;. The problem is that SonarCloud does not allow ingestion of OWASP Dependency Checker vulnerabilities out the box.&lt;/p&gt;
&lt;p&gt;However, there is the option to ingest &lt;a href=&#34;https://docs.sonarcloud.io/enriching/generic-issue-data/&#34;&gt;Generic Issue Data&lt;/a&gt;. To make use of this I just needed to change my XML results file to a JSON format&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The <a href="https://marketplace.visualstudio.com/items?itemName=dependency-check.dependencycheck">OWASP Dependency Checker</a> can be used to check for known vulnerabilities in a variety of eco-systems. This tool produces a HTML based report, but I wanted to expose the issues in <a href="https://sonarcloud.io">SonarCloud</a>. The problem is that SonarCloud does not allow ingestion of OWASP Dependency Checker vulnerabilities out the box.</p>
<p>However, there is the option to ingest <a href="https://docs.sonarcloud.io/enriching/generic-issue-data/">Generic Issue Data</a>. To make use of this I just needed to change my XML results file to a JSON format</p>
<script src="https://gist.github.com/rfennell/7a80189659f7fe128f29c71962b11c8e.js"></script>
<p>Once this was done the only remaining step was to tell SonarCloud where the coverted JSON file was</p>
<pre tabindex="0"><code># Additional properties that will be passed to the scanner, 
# Put one key=value per line, example:
# sonar.exclusions=**/*.bin
sonar.cpd.exclusions=**/AssemblyInfo.cs,**/*.g.cs, **/Migrations/**/*.cs
sonar.cs.vscoveragexml.reportsPaths=$(Agent.TempDirectory)/**/*.coveragexml
sonar.cs.vstest.reportsPaths=$(Agent.TempDirectory)/**/*.trx
# the full path location of the converted file
sonar.externalIssuesReportPaths=dependancy-results.json
</code></pre><p>Now the OWASP Dependency Checker vulnerabilities appear in SonarCloud, but with a few limitations</p>
<ul>
<li>Issues cannot be managed within SonarCloud; for instance, there is no ability to mark them as False Positive.</li>
<li>The activation of the rules that raise these issues cannot be managed within SonarCloud.</li>
<li>External rules are not visible on the Rules page or reflected in any Quality Profile.</li>
<li>My script only does a simple mapping of the different issue formats - but this could be modified to meet any other specific needs</li>
<li>Issues have to be mapped to a file already under analysis, you can&rsquo;t have general project issues.</li>
</ul>
<p>That all said, I think this is a nice solution to having a single dashboard for monitoring all my software supply chain issues.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Clearing the AssignedTo field on an Azure DevOps Work items with the AZ CLI</title>
      <link>https://blog.richardfennell.net/posts/clearing-the-assigned-to-field-with-az-cli/</link>
      <pubDate>Tue, 06 Sep 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/clearing-the-assigned-to-field-with-az-cli/</guid>
      <description>&lt;p&gt;In the past I &lt;a href=&#34;https://github.com/rfennell/AzureDevOpsPowershell/tree/a11eda67cfeb2ff34712b1423bdcb8f2cac28b0c/REST&#34;&gt;have written most of my Azure DevOps scripts&lt;/a&gt; calling the Azure DevOps REST API from PowerShell. This has worked, but did involve a lot of JSON payload handling.&lt;/p&gt;
&lt;p&gt;A better option these days is to look at the &lt;a href=&#34;https://docs.microsoft.com/en-us/cli/azure/&#34;&gt;AZ CLI&lt;/a&gt; and specifically the &lt;code&gt;azure-devops&lt;/code&gt; extension, as this does much of the heavy lifting for you.&lt;/p&gt;
&lt;p&gt;This does not mean that everything is plain sailing though. Today I hit a problem that took me a while to solve.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In the past I <a href="https://github.com/rfennell/AzureDevOpsPowershell/tree/a11eda67cfeb2ff34712b1423bdcb8f2cac28b0c/REST">have written most of my Azure DevOps scripts</a> calling the Azure DevOps REST API from PowerShell. This has worked, but did involve a lot of JSON payload handling.</p>
<p>A better option these days is to look at the <a href="https://docs.microsoft.com/en-us/cli/azure/">AZ CLI</a> and specifically the <code>azure-devops</code> extension, as this does much of the heavy lifting for you.</p>
<p>This does not mean that everything is plain sailing though. Today I hit a problem that took me a while to solve.</p>
<p>Today, I wanted to use the AZ CLI to remove assigned identity from a work item.</p>
<p>There is a parameter to assign the WI owner on the <code>az boards work-item update</code> command</p>
<p><code>az boards work-item update --id 123 --assigned-to myname</code></p>
<p>But I could not find a way to pass a null/empty/unassigned to clear this value.</p>
<p>In the end I found the answer was to use the following form, editing the field by name</p>
<p><code>az boards work-item update --id 123 --fields &quot;System.AssignedTo=&quot;</code></p>
<p>Note, that there is nothing after the <code>=</code>, no space, null or empty quotes (and yes I had tried all of those first)</p>
<p>Hope this post means someone gets to this solution quicker than I did.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why has my HP printer become a DVD? A fix for HP USB Printers not being detected on Windows 10/11</title>
      <link>https://blog.richardfennell.net/posts/why-has-my-printer-become-a-drive/</link>
      <pubDate>Sat, 30 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-has-my-printer-become-a-drive/</guid>
      <description>&lt;p&gt;Today I made the fateful mistake of offering to try to fix a family members home printer. Family IT, and especially printers, the bane of all IT Professionals.&lt;/p&gt;
&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;The system in question was a 10 year old setup made up of a Dell Optiplex desktop currently running Windows 10 and an HP M1132 LaserJet multifunction printer. This had all been working until a couple of weeks ago when the PC failed to detect the printer. The printer was uninstalled, assuming that would fix the problem, but the Add Printer tools could not even find the printer.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Today I made the fateful mistake of offering to try to fix a family members home printer. Family IT, and especially printers, the bane of all IT Professionals.</p>
<h1 id="the-problem">The Problem</h1>
<p>The system in question was a 10 year old setup made up of a Dell Optiplex desktop currently running Windows 10 and an HP M1132 LaserJet multifunction printer. This had all been working until a couple of weeks ago when the PC failed to detect the printer. The printer was uninstalled, assuming that would fix the problem, but the Add Printer tools could not even find the printer.</p>
<p>I tried all the normal things, power cycle everything, swap the USB cable, reinstalling drivers, tested the printer on my Windows 11 laptop, all to no effect.</p>
<p>I was close to giving up, assuming the USB circuitry on the printer must have failed. However, just before I gave up, I noticed that when you plugged the USB printer cable in a DVD was detected by the PC. HP used this means, or at least did 10 years ago, to ship the drivers and other HP bloatware they deemed necessary. Until recently after this DVD was detected, Windows when onto detecting the printer. It was this second step that had failed, and I suspect the cause was a recent Windows (10 &amp; 11) Security Update.</p>
<h1 id="the-solution">The Solution</h1>
<p>Turns out <a href="https://h30434.www3.hp.com/t5/LaserJet-Printing/Re-Windows-Recognize-my-printer-as-cd-drive/td-p/6845785">the solution</a> was in the HP Support Forum, but it took some finding as the forums are full of wrong answer, half solutions and confusion.</p>
<p>Basically you need to run a <a href="https://ftp.hp.com/pub/softlib/software12/COL53553/Im-129228-1/LJM1130_M1210_SI_Utility.exe">utility</a> that disable the HP Smart Install DVD feature, so the printer does not appear as a DVD. Once this was done the expected Windows Add Printer Wizard worked as expected.</p>
<p>So that was two hours of my life I won&rsquo;t get back, and I do wonder how many older HP printers will end up landfill due to what I assume is a Windows Security update to block some autorun run vulnerability?</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why has my MVC site stopped working?</title>
      <link>https://blog.richardfennell.net/posts/why-has-my-mvc-site-stopped-working/</link>
      <pubDate>Thu, 28 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/why-has-my-mvc-site-stopped-working/</guid>
      <description>&lt;p&gt;I am currently upgrading a .NET Core 3.1(LTS) MVC website to run on .NET 6, stepping it through intermediate .NET versions to make sure some EF Migrations were done correctly.&lt;/p&gt;
&lt;p&gt;Everything upgraded without any major issue until the final step to .NET 6. As soon as I did this my MVC pages failed to render, but no error was reported&lt;/p&gt;
&lt;p&gt;After much fiddling the solution to the problem was pointed out to me by one of my colleagues. The way MVC is shipped with .NET (Core) has changed from being a NuGet package to being part of the framework. This happened a good while ago, but become a blocking issue with .NET Core.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I am currently upgrading a .NET Core 3.1(LTS) MVC website to run on .NET 6, stepping it through intermediate .NET versions to make sure some EF Migrations were done correctly.</p>
<p>Everything upgraded without any major issue until the final step to .NET 6. As soon as I did this my MVC pages failed to render, but no error was reported</p>
<p>After much fiddling the solution to the problem was pointed out to me by one of my colleagues. The way MVC is shipped with .NET (Core) has changed from being a NuGet package to being part of the framework. This happened a good while ago, but become a blocking issue with .NET Core.</p>
<p>The fix was in the end simple, to remove the NuGet package references to the Microsoft.AspNetCore.* 2.2.0. Once this was done the unedited MVC pages loaded using the previous working methods. I did do some further tidying to use the current MVC controller methods, but this was not essential as the old methods are now just alias for the new methods.</p>
<p>So the lesson learnt?</p>
<p>Pay more attention to the various &lsquo;Migrate from version of .NET Core to a later version steps&rsquo; documents, and don&rsquo;t put off making changes for soon to be deprecated features, as it will trip you up in a confusing manner later.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cannot retrieve Umbraco node</title>
      <link>https://blog.richardfennell.net/posts/cannot-retrieve-umbraco-node/</link>
      <pubDate>Mon, 25 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/cannot-retrieve-umbraco-node/</guid>
      <description>&lt;p&gt;We recently hit a problem when we tried to edit a page on anold Umbraco 7 instance. When we tried to edit a page in the Umbraco web UI we got the error &amp;lsquo;failed to retrieve data for content id 1119&amp;rsquo;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;Umbraco Error&#34; loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/images/rfennell/umbracoerror.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;Now this page had been created a long time ago by a user who had since left the company, and this was the root cause. It seems there is an issue with Umbraco and deleted users. To avoid this problem, it is actually recommended you disable old Umbraco user accounts as opposed to deleting them.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>We recently hit a problem when we tried to edit a page on anold Umbraco 7 instance. When we tried to edit a page in the Umbraco web UI we got the error &lsquo;failed to retrieve data for content id 1119&rsquo;</p>
<p><img alt="Umbraco Error" loading="lazy" src="/images/rfennell/umbracoerror.png"></p>
<p>Now this page had been created a long time ago by a user who had since left the company, and this was the root cause. It seems there is an issue with Umbraco and deleted users. To avoid this problem, it is actually recommended you disable old Umbraco user accounts as opposed to deleting them.</p>
<p>The fix was a bit of SQL to reassign the problem node to a valid user ID</p>
<pre tabindex="0"><code>update [dbo].[umbracoNode] set nodeUser = 8  where where id = 1119
</code></pre><p>Once this script was run, and I restarted the Azure hosted Web-App, to reload the cache, I was able to edit the problematic node</p>
]]></content:encoded>
    </item>
    <item>
      <title>Social Media Posts after Migrating from WordPress to Hugo Static Pages</title>
      <link>https://blog.richardfennell.net/posts/social-media-posts-after-migrating-from-wordpress-to-hugo/</link>
      <pubDate>Thu, 07 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/social-media-posts-after-migrating-from-wordpress-to-hugo/</guid>
      <description>&lt;p&gt;I &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/migrating-from-wordpress-to-hugo/&#34;&gt;posted recently on my experience moving to Hugo from WordPress&lt;/a&gt;. One feature lost in the move were the Wordpress plugins used to automatically post to Twitter and LinkedIn when a new blog post was created. I always found this very useful, so looked or a way to replicate this functionality for static pages.&lt;/p&gt;
&lt;p&gt;The solution I ended up with was &lt;a href=&#34;https://azure.microsoft.com/en-gb/services/logic-apps/&#34;&gt;Azure Logic Apps&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I created a Logic App with a scheduled triggered that checked my blogs&amp;rsquo;s RSS feed every 30 minutes. If it found the RSS feed had been updated, I then created a bitly link for the new post&amp;rsquo;s URL, then posts to Email (as a test), Twitter and LinkedIn.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I <a href="https://blogs.blackmarble.co.uk/rfennell/migrating-from-wordpress-to-hugo/">posted recently on my experience moving to Hugo from WordPress</a>. One feature lost in the move were the Wordpress plugins used to automatically post to Twitter and LinkedIn when a new blog post was created. I always found this very useful, so looked or a way to replicate this functionality for static pages.</p>
<p>The solution I ended up with was <a href="https://azure.microsoft.com/en-gb/services/logic-apps/">Azure Logic Apps</a>.</p>
<p>I created a Logic App with a scheduled triggered that checked my blogs&rsquo;s RSS feed every 30 minutes. If it found the RSS feed had been updated, I then created a bitly link for the new post&rsquo;s URL, then posts to Email (as a test), Twitter and LinkedIn.</p>
<p>The really nice thing is this is done with built in Logic App connectors, so was quick an easy to create.</p>
<p><img alt="Logic App" loading="lazy" src="/images/rfennell/LogicAppScreenShot.png"></p>
]]></content:encoded>
    </item>
    <item>
      <title>Migrating from WordPress to Hugo Static Pages</title>
      <link>https://blog.richardfennell.net/posts/migrating-from-wordpress-to-hugo/</link>
      <pubDate>Fri, 01 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/migrating-from-wordpress-to-hugo/</guid>
      <description>&lt;h1 id=&#34;background&#34;&gt;Background&lt;/h1&gt;
&lt;p&gt;Over the years, the Black Marble blog server has been hosted on many platforms. &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/10/18/moving-bm-bloggers-from-blogengine-net-to-wordpress/&#34;&gt;It&amp;rsquo;s previous incarnation was WordPress&lt;/a&gt;, running as a network of sites with an aggregated feed. Of late we had found this slow to serve the first page (due to website start-up time) and there was the constant need to keep the instance patched.&lt;/p&gt;
&lt;p&gt;The time had come for a new solutions, and we picked &lt;a href=&#34;https://gohugo.io/&#34;&gt;Hugo Static Pages&lt;/a&gt;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="background">Background</h1>
<p>Over the years, the Black Marble blog server has been hosted on many platforms. <a href="https://blogs.blackmarble.co.uk/rfennell/2017/10/18/moving-bm-bloggers-from-blogengine-net-to-wordpress/">It&rsquo;s previous incarnation was WordPress</a>, running as a network of sites with an aggregated feed. Of late we had found this slow to serve the first page (due to website start-up time) and there was the constant need to keep the instance patched.</p>
<p>The time had come for a new solutions, and we picked <a href="https://gohugo.io/">Hugo Static Pages</a>.</p>
<h1 id="migration-process">Migration Process</h1>
<p>The process to migrate our content was not as hard as I had feared.</p>
<ol>
<li>The main (aggregate of all active sites) blog was exported using <a href="https://wordpress.com/support/export/">Wordpress&rsquo;s export feature</a> as an XML file.</li>
<li>The read only archives of blogs of ex. staff were individually exported, as they did not appear in the aggregate export file.</li>
<li>The exported XML content was converted to markdown using <a href="https://github.com/palaniraja/blog2md">blog2md</a>. <strong>Note:</strong> that a small edit was made to the <a href="https://gist.github.com/rfennell/0f2768e5e6da0c1eb384e62e2f632116">blog2md tool to place the contents of each sub blog in a separate folder</a></li>
<li>I wrote a <a href="https://gist.github.com/rfennell/40d43afff81809447d30753056a5e64f">PowerShell script to add an <code>alias</code> entry to each file so the old Wordpress permalinks were still valid</a>.</li>
<li>A new Hugo blog was created using the <a href="https://github.com/chipzoller/hugo-clarity">hugo-clarity theme</a> as this was a nice clean blog style theme.</li>
<li>I copied the exported folders (created by blog2md) into the <code>content</code> folder of the new site</li>
<li>I copied the <code>wp_content/uploads</code> folder structure containing all the Wordpress uploaded images to the <code>static</code> folder of the new site. This allowed any images to be found without the need to edit the <code>&lt;image src=...</code> settings in the post files.</li>
<li>Edited the Hugo site to
<ul>
<li>Update styles/images to our branding</li>
<li>Removed sample site pages we did not need</li>
<li>Add a data structure to define the blogs on our site e.g. name, titles, github IDs etc.</li>
<li>Created a page to list the blogs based on the new data file.</li>
<li>Edited the partial html blocks to render the RHS pane as we needed it.</li>
</ul>
</li>
</ol>
<p>The site could now be tested locally. The next step was to publish it to Azure.</p>
<h1 id="hosting">Hosting</h1>
<p>We had planned to use <a href="https://azure.microsoft.com/en-us/services/app-service/static/">Azure Static Web Sites</a> to do the hosting as these are cheap, fast and <a href="https://docs.microsoft.com/en-us/azure/static-web-apps/publish-hugo">the process is well documented</a>. However we had a problem, our site was too big.</p>
<p>The largest site allowed in Azure Static Web Sites is 500Mb, our blog site was over 1Gb with the numerous pages and associated image content. Hugo generated sites are not small.</p>
<p>This meant we had to host the site in an Azure Web Site, we picked a Linux one, set to run PHP8. This setting is required because the Linux Web Site must have some execution engine selected, even though you are not using it. In effect enabling nginx. If this is not done then the static pages are not shown. There is a further issue with this type of hosting, related to nginx&rsquo;s handling of trailing slashes in URLs. The fix suggested <a href="https://www.blimped.nl/fixing-nginx-trailing-slash-issue-on-azure-linux-web-apps/">here</a> addressed the problem.</p>
<p>The move to a Azure Web Site necessitated editing the Azure Static Web Site generated Azure DevOps Pipeline. We ended up with a Pipeline that</p>
<ol>
<li>Clone the repo</li>
<li>Downloads the Hugo tools</li>
<li>Runs the Hugo command to generate the static pages</li>
<li>Zip&rsquo;s up the generated pages</li>
<li>Uses <a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep">Bicep </a> to build the Azure Web Site</li>
<li>Uses Zip deployment to push the pages to the site.</li>
</ol>
<h1 id="summary">Summary</h1>
<p>I am really pleased with this migration. We have ended up with a faster site with a far lower running and maintenance cost.</p>
<p>It was no where near as hard as I had feared, given we had a aggregated network of sites; and we managed to keep all the old permalinks with the exception of the RSS file with changed form <code>/feed/</code> to <code>/feed.xml</code>, but I can live with that. We don&rsquo;t see much RSS usage these days anyway.</p>
<p>So all in all well worth the effort.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Don&#39;t skimp on resources for GHES for demo instances</title>
      <link>https://blog.richardfennell.net/posts/dont-skimp-on-resources-for-ghes-for-demo-instances/</link>
      <pubDate>Thu, 16 Jun 2022 09:53:27 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/dont-skimp-on-resources-for-ghes-for-demo-instances/</guid>
      <description>&lt;p&gt;I wanted to have a look at some GitHub Enterprise Server (GHES) upgrade scenarios so decided to create a quick GHES install on my local test Hyper-V instance. Due to me skimping on resources, and making a typo, creating this instance was much harder than it should have been.&lt;/p&gt;
&lt;p&gt;The first issue was I gave it a tiny data disk, this was down to me making a typo in my GB to Bytes conversion when specifying the size. Interestingly, the GHES setup does not initially complain but sits on the &amp;lsquo;reloading system services&amp;rsquo; stage until it times out. If you check the &lt;em&gt;/setup/config.log&lt;/em&gt; you see many Nomad related 500 errors. A reboot of the VM showed the real problem, the log then showed plenty of out-of-disk space messages.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I wanted to have a look at some GitHub Enterprise Server (GHES) upgrade scenarios so decided to create a quick GHES install on my local test Hyper-V instance. Due to me skimping on resources, and making a typo, creating this instance was much harder than it should have been.</p>
<p>The first issue was I gave it a tiny data disk, this was down to me making a typo in my GB to Bytes conversion when specifying the size. Interestingly, the GHES setup does not initially complain but sits on the &lsquo;reloading system services&rsquo; stage until it times out. If you check the <em>/setup/config.log</em> you see many Nomad related 500 errors. A reboot of the VM showed the real problem, the log then showed plenty of out-of-disk space messages.</p>
<p>reloading system devices does take a while</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2022/06/image-1024x785.png"></p>
<p>The easiest fix was to just start again with a data disk of a reasonable size</p>
<p>I next hit the problems due to my skimping on resources. I am not sure why I chose to limit them, old habits of using systems with scarce resources I guess.</p>
<p>I had only given the VM 10Gb of memory and 1 CPU. The Hyper-V host was not production-grade, but could certainly supply more than that.</p>
<ul>
<li>The lack of at least 14Gb causes the GHES to fail to boot with a nice clear error message</li>
<li>The single CPU meant the &lsquo;reloading application services&rsquo; step fails, the <em>/setup/config.log</em> shows the message</li>
</ul>
<pre tabindex="0"><code>Task Group &#34;treelights&#34; (failed to place 1 allocation):  
* Resources exhausted on 1 nodes  
* Dimension &#34;cpu&#34; exhausted on 1 nodes
</code></pre><p>As soon as I stopped the VM and provided 14Gb of memory and multiple vCPU to the VM instance and rebooted the setup completed as expected.</p>
<p>So the top tip is to <a href="https://docs.github.com/en/enterprise-server@3.4/admin/installation/setting-up-a-github-enterprise-server-instance/installing-github-enterprise-server-on-hyper-v#minimum-requirements">read the GHES systems requirements</a> and actually follow them, even if it is just a test/demo instance.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fix for cannot &#39;TypeError: Cannot read property&#39; when Dependabot submits a PR to upgrade a Jest Module</title>
      <link>https://blog.richardfennell.net/posts/fix-for-cannot-typeerror-cannot-read-property-when-dependabot-submits-a-pr-to-upgrade-a-jest-module/</link>
      <pubDate>Thu, 19 May 2022 07:43:58 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fix-for-cannot-typeerror-cannot-read-property-when-dependabot-submits-a-pr-to-upgrade-a-jest-module/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/&#34;&gt;GitHub&amp;rsquo;s Dependabot&lt;/a&gt; is a great tool to help keep your dependencies up to date, and most of the time the PR it generates just merges without a problem. However, sometimes there are issues with other related dependencies.&lt;/p&gt;
&lt;p&gt;This was the case with a recent PR to update jest-circus to 28.x. The PR failed with the error&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TypeError: Cannot read property &amp;rsquo;enableGlobally&amp;rsquo; of undefined at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:39:25) at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13) at runJest (node_modules/@jest/core/build/runJest.js:404:19) at _run10000 (node_modules/@jest/core/build/cli/index.js:320:7) at runCLI (node_modules/@jest/core/build/cli/index.js:173:3)&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/">GitHub&rsquo;s Dependabot</a> is a great tool to help keep your dependencies up to date, and most of the time the PR it generates just merges without a problem. However, sometimes there are issues with other related dependencies.</p>
<p>This was the case with a recent PR to update jest-circus to 28.x. The PR failed with the error</p>
<blockquote>
<p>TypeError: Cannot read property &rsquo;enableGlobally&rsquo; of undefined at jestAdapter (node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:39:25) at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13) at runJest (node_modules/@jest/core/build/runJest.js:404:19) at _run10000 (node_modules/@jest/core/build/cli/index.js:320:7) at runCLI (node_modules/@jest/core/build/cli/index.js:173:3)</p></blockquote>
<p>In the end, the fix was simple, make sure all the other Jest related packages were updated to 28.x versions. Once I did this, using a <a href="https://github.com/features/codespaces">GitHub Codespace</a>, the PR merged without a problem.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fix for Azure DevOps deployment to an environment stuck in &#34;Job is pending&#34; state</title>
      <link>https://blog.richardfennell.net/posts/fixe-for-azure-devops-deployment-to-an-environment-stuck-in-job-is-pending-state/</link>
      <pubDate>Thu, 07 Apr 2022 12:06:53 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fixe-for-azure-devops-deployment-to-an-environment-stuck-in-job-is-pending-state/</guid>
      <description>&lt;h2 id=&#34;issue&#34;&gt;Issue&lt;/h2&gt;
&lt;p&gt;I had an Azure DevOps YAML based pipeline that had been working but was now getting stuck with the message &amp;ldquo;Job is pending&amp;hellip;&amp;rdquo; when trying to start a stage in which there is a deployment to an &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/environments&#34;&gt;environment&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Looking at the logs and Azure DevOps UI it was not obvious what the issue was.&lt;/p&gt;
&lt;h2 id=&#34;solution&#34;&gt;Solution&lt;/h2&gt;
&lt;p&gt;Turns out it was due to &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;amp;tabs=check-pass&#34;&gt;environment checks and approvals&lt;/a&gt;. There was a branch policy on the environment. This was set to only allow use of Azure DevOps Templates on a given branch. The edit that had been done to the YAML meant it was trying to extend a template in a branch that was not in the approved list.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="issue">Issue</h2>
<p>I had an Azure DevOps YAML based pipeline that had been working but was now getting stuck with the message &ldquo;Job is pending&hellip;&rdquo; when trying to start a stage in which there is a deployment to an <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/environments">environment</a>.</p>
<p>Looking at the logs and Azure DevOps UI it was not obvious what the issue was.</p>
<h2 id="solution">Solution</h2>
<p>Turns out it was due to <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;tabs=check-pass">environment checks and approvals</a>. There was a branch policy on the environment. This was set to only allow use of Azure DevOps Templates on a given branch. The edit that had been done to the YAML meant it was trying to extend a template in a branch that was not in the approved list.</p>
<p>As soon as the working branch was added to the proved list it all worked as expected.</p>
<p>So if you see &ldquo;Job is pending&rdquo; errors with no obvious reason, check the environment approvals and checks. Remember, any issues with these don&rsquo;t show up in the build log</p>
]]></content:encoded>
    </item>
    <item>
      <title>A workaround for not being able to access custom variables via stagedependencies if they are set in deployment jobs in Azure DevOps Pipelines</title>
      <link>https://blog.richardfennell.net/posts/a-workaround-for-not-being-able-to-access-custom-variables-via-stagedependencies-if-they-are-set-in-deployment-jobs-in-azure-devops-pipelines/</link>
      <pubDate>Sat, 19 Feb 2022 16:37:19 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/a-workaround-for-not-being-able-to-access-custom-variables-via-stagedependencies-if-they-are-set-in-deployment-jobs-in-azure-devops-pipelines/</guid>
      <description>&lt;p&gt;I have blogged in the past ( &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2020/11/27/getting-confused-over-azure-devops-pipeline-variable-evaluation/&#34;&gt;here&lt;/a&gt; and &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2022/01/10/using-azure-devops-stage-dependency-variables-with-conditional-stage-and-job-execution/&#34;&gt;here&lt;/a&gt;) about the complexities and possible areas of confusion with different types of Azure DevOps pipeline variables. I have also &lt;a href=&#34;https://developercommunity.visualstudio.com/t/unable-to-retrieve-stage-result-from-stagedependen/1064759#T-N1130023&#34;&gt;seen issues raised&lt;/a&gt; over how to access custom variables across jobs and stages. Safe to say, this is an area where it is really easy to get it wrong and end up with a null value.&lt;/p&gt;
&lt;p&gt;I have recently come across another edge case to add to the list of gotchas.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have blogged in the past ( <a href="https://blogs.blackmarble.co.uk/rfennell/2020/11/27/getting-confused-over-azure-devops-pipeline-variable-evaluation/">here</a> and <a href="https://blogs.blackmarble.co.uk/rfennell/2022/01/10/using-azure-devops-stage-dependency-variables-with-conditional-stage-and-job-execution/">here</a>) about the complexities and possible areas of confusion with different types of Azure DevOps pipeline variables. I have also <a href="https://developercommunity.visualstudio.com/t/unable-to-retrieve-stage-result-from-stagedependen/1064759#T-N1130023">seen issues raised</a> over how to access custom variables across jobs and stages. Safe to say, this is an area where it is really easy to get it wrong and end up with a null value.</p>
<p>I have recently come across another edge case to add to the list of gotchas.</p>
<p>It seems you cannot use <strong>stagedependencies</strong> to access a variable declared in a <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/deployment-jobs?view=azure-devops">deployment job</a> i.e. when you are using an <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/environments?view=azure-devops">environment</a> to get approval for a release.</p>
<p>The workaround is to add a job that is dependent on the deployment and set the custom variable within it. This variable can be accessed by a later stage as shown below</p>
<pre tabindex="0"><code>- stage: S1
  jobs:
  - deployment: D1
    strategy:
      runOnce:
        deploy:
          steps:
              - checkout: none
              - bash: echo &#34;Can&#39;t access the variable if set in here&#34;
  - job: J1
    dependsOn:
      D1
    steps:
      - checkout: none
      - bash: echo &#34;##vso[task.setvariable variable=myvar;isOutput=true]True&#34; 
        name: BashStep

- stage: S2
  condition: always()

  dependsOn: 
   - S1
  jobs:
   - job: Use_Variable
     variables: # add an alias for the var
       myvar: $[stagedependencies.S1.J1.outputs[&#39;BashStep.myvar&#39;]]
        steps:
          - checkout: none
          - dash: echo &#34;Script gets run when myvar is true&#34;
            condition: eq (variables[&#39;myvar&#39;],&#39;True&#39;)
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>The importance of blogging - or how to do your future self a favour</title>
      <link>https://blog.richardfennell.net/posts/the-importance-of-blogging-or-how-to-do-your-future-self-a-favour/</link>
      <pubDate>Fri, 14 Jan 2022 10:34:33 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/the-importance-of-blogging-or-how-to-do-your-future-self-a-favour/</guid>
      <description>&lt;p&gt;Yesterday, yet again, I was thankful for my past self taking time to blog about a technical solution I had found.&lt;/p&gt;
&lt;p&gt;I had an error when trying to digitally sign a package. On searching on the error code I came across my &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2019/04/30/a-fix-for-error-signersign-failed-2146958839-0x80080209-with-signtool-exe/&#34;&gt;own blog post&lt;/a&gt; with the solution. This was, as usual, one I had no recollection of writing.&lt;/p&gt;
&lt;p&gt;I find this happens all the time. It is a little disturbing when you search for an issue and the only reference is to a post you made and have forgotten, so you are the defacto expert, nobody knows anymore on the subject, but better than having no solution.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Yesterday, yet again, I was thankful for my past self taking time to blog about a technical solution I had found.</p>
<p>I had an error when trying to digitally sign a package. On searching on the error code I came across my <a href="https://blogs.blackmarble.co.uk/rfennell/2019/04/30/a-fix-for-error-signersign-failed-2146958839-0x80080209-with-signtool-exe/">own blog post</a> with the solution. This was, as usual, one I had no recollection of writing.</p>
<p>I find this happens all the time. It is a little disturbing when you search for an issue and the only reference is to a post you made and have forgotten, so you are the defacto expert, nobody knows anymore on the subject, but better than having no solution.</p>
<p>Too often I ask people if they have documented the hints, tips and solutions they find and the response I get is &lsquo;I will remember&rsquo;. Trust me you won&rsquo;t. Write something down where it is discoverable for your team and your future self. This can be any format that works for you: an Email, OneNote, a Wiki or the one I find most useful a blog. Just make sure it is easily searchable.</p>
<p>Your future self will thank you.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using Azure DevOps Stage Dependency Variables with Conditional Stage and Job Execution</title>
      <link>https://blog.richardfennell.net/posts/using-azure-devops-stage-dependency-variables-with-conditional-stage-and-job-execution/</link>
      <pubDate>Mon, 10 Jan 2022 13:16:46 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-azure-devops-stage-dependency-variables-with-conditional-stage-and-job-execution/</guid>
      <description>&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/wp-content/uploads/sites/2/2022/01/image-1-1024x446.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;I have been doing some work with Azure DevOps multi-stage YAML pipelines using stage dependency variables and conditions. They can get confusing quickly, you need one syntax in one place and another elsewhere.&lt;/p&gt;
&lt;p&gt;So, here are a few things I have learnt&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;what-are-stage-dependency-variables&#34;&gt;What are stage dependency variables?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/stages?view=azure-devops&amp;amp;tabs=yaml#specify-dependencies&#34;&gt;Stage Dependencies&lt;/a&gt; are the way you define which stage follows another in a multi-stage YAML pipeline. This is as opposed to just relying on the order they appear in the YAML file, the default order. Hence, they are critical to creating complex pipelines.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img loading="lazy" src="/wp-content/uploads/sites/2/2022/01/image-1-1024x446.png"></p>
<p>I have been doing some work with Azure DevOps multi-stage YAML pipelines using stage dependency variables and conditions. They can get confusing quickly, you need one syntax in one place and another elsewhere.</p>
<p>So, here are a few things I have learnt&hellip;</p>
<h2 id="what-are-stage-dependency-variables">What are stage dependency variables?</h2>
<p><a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/stages?view=azure-devops&amp;tabs=yaml#specify-dependencies">Stage Dependencies</a> are the way you define which stage follows another in a multi-stage YAML pipeline. This is as opposed to just relying on the order they appear in the YAML file, the default order. Hence, they are critical to creating complex pipelines.</p>
<p><a href="https://developercommunity.visualstudio.com/t/unable-to-retrieve-stage-result-from-stagedependen/1064759">Stage Dependency variables</a> are the way you can pass variables from one stage to another. Special handling is required, as you can’t just use the ordinary <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&amp;tabs=yaml%2Cbatch#use-output-variables-from-tasks">output variables</a> (which are in effect environment variables on the agent) as you might within a job as there is no guarantee the stages and jobs are running on the same agent.</p>
<p>For stage dependency variables, is not how you create output variables, that does not differ from the standard manner, the difference is in how you retrieve them.</p>
<p><a href="https://gist.github.com/rfennell/b57db0c2e4e3bae1968a4908b0df3595">In my sample</a>, I used a BASH script to set the output variable based on a parameter passed into the pipeline, but you can create output variables using scripts or tasks</p>
<pre tabindex="0"><code> - stage: SetupStage
    displayName: &#39;Setup Stage&#39;
    jobs:
      - job: SetupJob
        displayName: &#39;Setup Job&#39;
        steps:
          - checkout: none
          - bash:  |
              set -e # need to avoid trailing &#34; being added to the variable https://github.com/microsoft/azure-pipelines-tasks/issues/10331
              echo &#34;##vso[task.setvariable variable=MyVar;isOutput=true]${{parameters.value}}&#34;
            name: SetupStep
            displayName: &#39;Setup Step&#39; 
</code></pre><h2 id="possible-ways-to-access-a-stage-dependency-variable">Possible ways to access a stage dependency variable</h2>
<p>There are two basic ways to access stage dependency variables, both using array objects</p>
<pre tabindex="0"><code>stageDependencies.STAGENAME.JOBNAME.outputs[&#39;STEPNAME.VARNAME&#39;]
dependencies.STAGENAME.outputs[&#39;JOBNAME.STEPNAME.VARNAME&#39;]
</code></pre><p>Which one you use, in which place, and whether via a local alias is the complexity</p>
<h2 id="how-to-access-a-stage-dependency-in-a-script">How to access a stage dependency in a script?</h2>
<p>To access a stage dependency variable in a script, or a task, there are two key requirements</p>
<ul>
<li>The stage containing the consuming job and hence script/task, must be set as dependant on the stage that created the output variable</li>
<li>You have to declare a local alias for the value in the <strong>stageDependencies</strong> array within the consuming stage. This local alias will be used as the local name by scripts and tasks</li>
</ul>
<p>Once this is configured you can access the variable like any other local YAML variable</p>
<pre tabindex="0"><code> - stage: Show_With_Dependancy
    displayName: ‘Show Stage With dependancy’
    dependsOn:
      - SetupStage
    variables:
      localMyVarViaStageDependancies : $[stageDependencies.SetupStage.SetupJob.outputs[‘SetupStep.MyVar’]]
    jobs:
      - job: Job
        displayName: ‘Show Job With dependancy’
        steps:
        - bash: |
              echo “localMyVarViaStageDependancies - $(localMyVarViaStageDependancies)” 
</code></pre><p><strong>Tip:</strong> If you are having a problem with the value not being set for a stage dependency variable look in the pipeline execution log, at the job level, and check the ‘Job preparation parameters’ section to see what is being evaluated. This will show if you are using the wrong array object, or have a typo, as any incorrect declarations evaluate as null</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2022/01/image.png"></p>
<h2 id="how-to-use-a-stage-dependency-as-a-stage-condition">How to use a stage dependency as a stage condition</h2>
<p>You can use stage dependency variables as controlling conditions for running a stage. In this use-case you use the <strong>dependencies</strong> array and not the <strong>stagedependencies</strong> used when aliasing variables.</p>
<pre tabindex="0"><code> - stage: Show_With_Dependancy_Condition
    condition: and (succeeded(), eq (dependencies.SetupStage.outputs[&#39;SetupJob.SetupStep.MyVar&#39;], &#39;True&#39;))
    displayName: &#39;Show Stage With dependancy Condition&#39; 
</code></pre><p>From my experiments for this use-case, you don’t seem to need the <strong>DependsOn</strong> entry to decare the stage that exposed the output variable for this to work. So, this is very useful for complex pipelines where you want to skip a later stage based on a much earlier stage for which there is no direct dependency.</p>
<p>A side effect of using a stage condition is that many subsequent stages have to have their execution conditions edited as you cannot rely on the default completion stage state <strong>succeeded.</strong> This is because the prior stages could now be <strong>succeeded</strong> or <strong>skipped</strong>. Hence all following stages need to use the condition</p>
<pre tabindex="0"><code>condition: and( not(failed()), not(canceled()))
</code></pre><h2 id="how-to-use-a-stage-dependency-as-a-job-condition">How to use a stage dependency as a job condition</h2>
<p>To avoid the need to alter all the subsequent stage&rsquo;s execution conditions you can set a condition at the job or task level. Unlike setting the condition at that stage level, you have to create a local alias (see above) and check the condition on that</p>
<pre tabindex="0"><code> - stage: Show_With_Dependancy_Condition_Job
    displayName: &#39;Show Stage With dependancy Condition&#39;
    dependsOn:
      - SetupStage
    variables:
      localMyVarViaStageDependancies : $[stageDependencies.SetupStage.SetupJob.outputs[&#39;SetupStep.MyVar&#39;]]
    jobs:
      - job: Job
        condition: and (succeeded(),
          eq (variables.localMyVarViaStageDependancies, &#39;True&#39;))
        displayName: &#39;Show Job With dependancy&#39; 
</code></pre><p>This technique will work for both <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/phases?view=azure-devops&amp;tabs=yaml#types-of-jobs">Agent-based and Agent-Less (Server) jobs</a></p>
<p>A warning though, if your job makes use of an <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/environments?view=azure-devops">environment</a> with a manual approval, the environment approval check is evaluated before the job condition. This is probably not what you are after, so if using conditions with environments that use manual approvals then the condition is probably best set at the stage level, with the knock-on issues of states of subsequent stages as mentioned above.</p>
<p>An alternative, if you are just using the environment for manual approval, is to look at using an AgentLess job with <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/manual-validation?view=azure-devops&amp;tabs=yaml">a manual approval</a>. AgentLess job manual approvals are evaluated after the job condition, so do not suffer the same problem.</p>
<p>If you need to use a stage dependency variable in a later stage, as a job condition or script variable, but do not wish to add a direct dependency between the stages, you could consider ‘republishing’ the variable as an output of the intermedia stage(s)</p>
<pre tabindex="0"><code> - stage: Intermediate_Stage
    dependsOn:
      - SetUpStage
    variables:
      localMyVarViaStageDependancies : $[stageDependencies.SetupStage.SetupJob.outputs[&#39;SetupStep.MyVar&#39;]]
    jobs:
      - job: RepublishMyVar
       steps:
          - checkout: none
          - bash:  |
              set -e # need to avoid trailing &#34; being added to the variable https://github.com/microsoft/azure-pipelines-tasks/issues/10331
              echo &#34;##vso[task.setvariable variable=MyVar;isOutput=true]$( localMyVarViaStageDependancies)&#34;
            name: RepublishStep 
</code></pre><h2 id="summing-up">Summing Up</h2>
<p>So I hope this post will help you, and the future me, navigate the complexities of stage variables</p>
<p><a href="https://gist.github.com/rfennell/b57db0c2e4e3bae1968a4908b0df3595">You can find the YAML for the test harness I have been using in this GitHub GIST</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Setting Azure DevOps &#39;All Repositories&#39; Policies via the CLI</title>
      <link>https://blog.richardfennell.net/posts/setting-azure-devops-all-repositories-policies-via-the-cli/</link>
      <pubDate>Fri, 12 Nov 2021 12:49:03 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/setting-azure-devops-all-repositories-policies-via-the-cli/</guid>
      <description>&lt;p&gt;The &lt;a href=&#34;https://docs.microsoft.com/en-us/cli/azure/devops?view=azure-cli-latest&#34;&gt;Azure DevOps CLI&lt;/a&gt; provides plenty of commands to update Team Projects, but it does not cover all things you might want to set. A good example is setting branch policies. For a given repo you can set the policies using the &lt;a href=&#34;https://docs.microsoft.com/en-us/cli/azure/service-page/azure%20repos?view=azure-cli-latest&#34;&gt;Azure Repo&lt;/a&gt; command eg:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;az repos policy approver-count update --project &amp;lt;projectname&amp;gt; --blocking true --enabled true --branch main --repository-id &amp;lt;guid&amp;gt; --minimum-approver-count w --reset-on-source-push true  --creator-vote-counts false --allow-downvotes false 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;However, you hit a problem if you wish to set the &amp;lsquo;All Repositories&amp;rsquo; policies for a Team Project. The issue is that the above command requires a specific &lt;strong&gt;--project&lt;/strong&gt; parameter.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The <a href="https://docs.microsoft.com/en-us/cli/azure/devops?view=azure-cli-latest">Azure DevOps CLI</a> provides plenty of commands to update Team Projects, but it does not cover all things you might want to set. A good example is setting branch policies. For a given repo you can set the policies using the <a href="https://docs.microsoft.com/en-us/cli/azure/service-page/azure%20repos?view=azure-cli-latest">Azure Repo</a> command eg:</p>
<pre tabindex="0"><code>az repos policy approver-count update --project &lt;projectname&gt; --blocking true --enabled true --branch main --repository-id &lt;guid&gt; --minimum-approver-count w --reset-on-source-push true  --creator-vote-counts false --allow-downvotes false 
</code></pre><p>However, you hit a problem if you wish to set the &lsquo;All Repositories&rsquo; policies for a Team Project. The issue is that the above command requires a specific <strong>--project</strong> parameter.</p>
<p>I can find no way around this using any published CLI tools, but using the <a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-6.1">REST API</a> there is an option.</p>
<p>You could of course check the API documentation to work out the exact call and payload. However, I usually find it quicker to perform the action I require in the Azure DevOps UI and monitor the network traffic in the browser developer tools to see what calls are made to the API.</p>
<p>Using this technique, I have created the following script that sets the All Repositories branch policies.</p>
<script src="https://gist.github.com/rfennell/def3ae3e7303db66f3eda3d2eb4a2475.js"></script>
<p>Note that you can use this same script to set a specific repo&rsquo;s branch policies by setting the <strong>repositoryId</strong> in the JSON payloads.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The case of the self-cancelling Azure DevOps pipeline</title>
      <link>https://blog.richardfennell.net/posts/the-case-of-the-self-cancelling-azure-devops-pipeline/</link>
      <pubDate>Fri, 29 Oct 2021 20:12:28 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/the-case-of-the-self-cancelling-azure-devops-pipeline/</guid>
      <description>&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;Today I came across a strange issue with a reasonably old multi-stage YAML pipeline, it appeared to be cancelling itself.&lt;/p&gt;
&lt;p&gt;The Build stage ran OK, but the Release stage kept being shown as cancelled with a strange error. The strangest thing was it did not happen all the time. I guess this is the reason the problem had not been picked up sooner.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/wp-content/uploads/sites/2/2021/10/image-1-1024x877.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;If I looked at the logs for the Release stage, I saw that the main job, and meant to be the only job, had completed successfully. But I had gained an extra unexpected job that was being cancelled in 90+% of my runs.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="the-issue">The Issue</h2>
<p>Today I came across a strange issue with a reasonably old multi-stage YAML pipeline, it appeared to be cancelling itself.</p>
<p>The Build stage ran OK, but the Release stage kept being shown as cancelled with a strange error. The strangest thing was it did not happen all the time. I guess this is the reason the problem had not been picked up sooner.</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2021/10/image-1-1024x877.png"></p>
<p>If I looked at the logs for the Release stage, I saw that the main job, and meant to be the only job, had completed successfully. But I had gained an extra unexpected job that was being cancelled in 90+% of my runs.</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2021/10/image-2-1024x787.png"></p>
<p>This extra job was trying to run on an Ubuntu hosted agent and failing to make a connection. All very strange as all the jobs were meant to be using private Windows-based agents.</p>
<h2 id="the-solution">The Solution</h2>
<p>Turns out, as you might expect, the issue was a typo in the YAML.</p>
<pre tabindex="0"><code>- stage: Release
  dependsOn: Build
  condition: succeeded()
  jobs:
  **- job:**
  - template: releasenugetpackage.yml@YAMLTemplates
    parameters:
</code></pre><p>The problem was the stray <strong>job:</strong> line. This was causing the attempt to connect to a hosted agent and then check out the code. Interesting a hosted Ubuntu agent was requested given there was no <strong>Pool</strong> defined</p>
<p>As soon as the extra line was removed the problems went away.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Making SonarQube Quality Checks a required PR check on Azure DevOps</title>
      <link>https://blog.richardfennell.net/posts/making-sonarqube-quality-checks-a-required-pr-check-on-azure-devops/</link>
      <pubDate>Tue, 21 Sep 2021 12:28:30 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/making-sonarqube-quality-checks-a-required-pr-check-on-azure-devops/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is another of those posts to remind me in the future. I searched the documentation for this answer for ages and found nothing, eventually getting the solution by asking on the &lt;a href=&#34;https://community.sonarsource.com/t/make-quality-gate-a-required-check-in-an-azure-devops-pr/49964&#34;&gt;SonarQube Forum&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When you link SonarQube into an Azure DevOps &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&#34;&gt;pipeline that is used from branch protection&lt;/a&gt; the success, or failure, of the PR branch analysis is shown as an optional PR Check&lt;/p&gt;
&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/wp-content/uploads/sites/2/2021/09/image.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;The question was &amp;lsquo;how to do I make it a required check?&amp;rsquo;. Turns out the answer is to add an extra Azure DevOps branch policey status check for the &amp;lsquo;SonarQube/quality gate&amp;rsquo;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>This is another of those posts to remind me in the future. I searched the documentation for this answer for ages and found nothing, eventually getting the solution by asking on the <a href="https://community.sonarsource.com/t/make-quality-gate-a-required-check-in-an-azure-devops-pr/49964">SonarQube Forum</a></em></p>
<p>When you link SonarQube into an Azure DevOps <a href="https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops">pipeline that is used from branch protection</a> the success, or failure, of the PR branch analysis is shown as an optional PR Check</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2021/09/image.png"></p>
<p>The question was &lsquo;how to do I make it a required check?&rsquo;. Turns out the answer is to add an extra Azure DevOps branch policey status check for the &lsquo;SonarQube/quality gate&rsquo;</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2021/09/image-1-1024x345.png"></p>
<p>When you press the + (add) button it turns out the &lsquo;SonarQube/quality gate&rsquo; is available in the drop-down</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2021/09/image-3-502x1024.png"></p>
<p>Once this change was made, the SonarQube Quality Check becomes a required PR Check.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How I dealt with a strange problem with PSRepositories and dotnet NuGet sources</title>
      <link>https://blog.richardfennell.net/posts/how-i-dealt-with-a-strange-problem-with-psrepositories-and-dotnet-nuget-sources/</link>
      <pubDate>Fri, 16 Jul 2021 15:03:17 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/how-i-dealt-with-a-strange-problem-with-psrepositories-and-dotnet-nuget-sources/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;We regularly re-build our Azure DevOps private agents using Packer and Lability, as I have &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2020/03/02/you-need-to-pass-a-github-pat-to-create-azure-devops-agent-images-using-packer/&#34;&gt;posted about before.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since the latest re-build, we have seen all sorts of problems. All related to pulling packages and tools from NuGet based repositories. Problems we have never seen with any previous generation of our agents.&lt;/p&gt;
&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;The issue turned out to be related to registering a private PowerShell repository.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$RegisterSplat = @{
Name = &amp;#39;PrivateRepo&amp;#39;
SourceLocation = &amp;#39;https://psgallery.mydomain.co.uk/nuget/PowerShell&amp;#39;
PublishLocation = &amp;#39;https://psgallery.mydomain.co.uk/nuget/PowerShell&amp;#39;
InstallationPolicy = &amp;#39;Trusted&amp;#39;
}

Register-PSRepository @RegisterSplat
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Running this command caused the default dotnet NuGet repository to be unregistered i.e. the command &lt;strong&gt;dotnet nuget list source&lt;/strong&gt; was expected to return&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="background">Background</h2>
<p>We regularly re-build our Azure DevOps private agents using Packer and Lability, as I have <a href="https://blogs.blackmarble.co.uk/rfennell/2020/03/02/you-need-to-pass-a-github-pat-to-create-azure-devops-agent-images-using-packer/">posted about before.</a></p>
<p>Since the latest re-build, we have seen all sorts of problems. All related to pulling packages and tools from NuGet based repositories. Problems we have never seen with any previous generation of our agents.</p>
<h2 id="the-issue">The Issue</h2>
<p>The issue turned out to be related to registering a private PowerShell repository.</p>
<pre tabindex="0"><code>$RegisterSplat = @{
Name = &#39;PrivateRepo&#39;
SourceLocation = &#39;https://psgallery.mydomain.co.uk/nuget/PowerShell&#39;
PublishLocation = &#39;https://psgallery.mydomain.co.uk/nuget/PowerShell&#39;
InstallationPolicy = &#39;Trusted&#39;
}

Register-PSRepository @RegisterSplat
</code></pre><p>Running this command caused the default dotnet NuGet repository to be unregistered i.e. the command <strong>dotnet nuget list source</strong> was expected to return</p>
<pre tabindex="0"><code>Registered Sources:
  1.  PrivateRepo
      https://psgallery.mydomain.co.uk/nuget/Nuget
  2.  nuget.org [Enabled]
      https://www.nuget.org/api/v2/
  3.  Microsoft Visual Studio Offline Packages [Enabled]
      C:Program Files (x86)Microsoft SDKsNuGetPackages 
</code></pre><p>But it returned</p>
<pre tabindex="0"><code>Registered Sources:
  1.  PrivateRepo
      https://psgallery.mydomain.co.uk/nuget/Nuget
  2.  Microsoft Visual Studio Offline Packages [Enabled]
      C:Program Files (x86)Microsoft SDKsNuGetPackages
</code></pre><h2 id="the-workaround">The Workaround</h2>
<p>You can&rsquo;t call this a solution, as I cannot see why it is really needed, but the following command does fix the problem</p>
<pre tabindex="0"><code> dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Automating the creation of Team Projects in Azure DevOps</title>
      <link>https://blog.richardfennell.net/posts/automating-the-creation-of-team-projects-in-azure-devops/</link>
      <pubDate>Thu, 10 Jun 2021 16:28:03 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/automating-the-creation-of-team-projects-in-azure-devops/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/organizations/projects/create-project?view=azure-devops&amp;amp;tabs=preview-page&#34;&gt;Creating a new project&lt;/a&gt; in Azure DevOps with your desired &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/organizations/settings/work/customize-process?view=azure-devops&#34;&gt;process template&lt;/a&gt; is straightforward. However, it is only the start of the job for most administrators. They will commonly want to set up other configuration settings such as &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&#34;&gt;branch protection rules&lt;/a&gt;, default pipelines etc. before giving the team access to the project. All this administration can be very time consuming and of course prone to human error.&lt;/p&gt;
&lt;p&gt;To make this process easier, quicker and more consistent I have developed a process to automated all of this work. It uses a mixture of the following:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://docs.microsoft.com/en-us/azure/devops/organizations/projects/create-project?view=azure-devops&amp;tabs=preview-page">Creating a new project</a> in Azure DevOps with your desired <a href="https://docs.microsoft.com/en-us/azure/devops/organizations/settings/work/customize-process?view=azure-devops">process template</a> is straightforward. However, it is only the start of the job for most administrators. They will commonly want to set up other configuration settings such as <a href="https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops">branch protection rules</a>, default pipelines etc. before giving the team access to the project. All this administration can be very time consuming and of course prone to human error.</p>
<p>To make this process easier, quicker and more consistent I have developed a process to automated all of this work. It uses a mixture of the following:</p>
<p><strong>A sample team project</strong> that contains a Git repo containing the base code I want in my new Team Project&rsquo;s default Git repo. In my case this includes</p>
<ul>
<li>An empty Azure Resource Management (ARM) template</li>
<li>A .NET Core Hello World console app with an associated .NET Core Unit Test project</li>
<li>A YAML pipeline to build and test the above items, as well as <a href="https://blogs.blackmarble.co.uk/rfennell/2020/04/22/i-decided-to-create-a-video-of-my-blog-post-on-multistage-yaml-pipelines/">generating release notes</a> into the Team Project WIKI</li>
</ul>
<p>A <strong>PowerShell script</strong> that uses both <a href="https://docs.microsoft.com/en-us/cli/azure/devops?view=azure-cli-latest">az devops</a> and the <a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-6.1">Azure DevOps REST API</a> to</p>
<ul>
<li>Create a new Team Project</li>
<li>Import the sample project Git repo into the new Team Project</li>
<li>Create a WIKI in the new Team Project</li>
<li>Add a <a href="https://www.sonarqube.org/">SonarQube</a>/<a href="https://sonarcloud.io/">SonarCloud</a> Service Endpoint</li>
<li>Update the YAML file for the pipeline to point to the newly created project resources</li>
<li>Update the branch protection rules</li>
<li>Grant access privaledges as needed for service accounts</li>
</ul>
<p>The script is far from perfect, it could do much more, but for me, it does the core requirements I need.</p>
<p>You could of course enhance it as required, removing features you don&rsquo;t need and adding code to do jobs such as <a href="https://github.com/rfennell/AzureDevOpsPowershell/blob/main/REST/Add-StandardBacklogTasks.ps1">adding any standard Work Items</a> you require at the start of a project. Or altering the contents of the sample repo to be cloned to better match your most common project needs.</p>
<p>You can find the PowerShell script in <a href="https://github.com/rfennell/AzureDevOpsPowershell/blob/main/REST/Create-TeamProject.ps1">AzureDevOpsPowershell GitHub repo</a>, hope you find it useful.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting the approver for release to an environment within an Azure DevOps Multi-Stage YAML pipeline</title>
      <link>https://blog.richardfennell.net/posts/getting-the-approver-for-release-to-an-environment-within-an-azure-devops-multi-stage-yaml-pipeline/</link>
      <pubDate>Sat, 15 May 2021 14:53:50 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-the-approver-for-release-to-an-environment-within-an-azure-devops-multi-stage-yaml-pipeline/</guid>
      <description>&lt;p&gt;I recently had the need to get the email address of the approver of a deployment to an environment from within a multi-stage YAML pipeline. Turns out it was not as easy as I might have hoped given the available documented APIs.&lt;/p&gt;
&lt;h3 id=&#34;background&#34;&gt;Background&lt;/h3&gt;
&lt;p&gt;My YAML pipeline included a &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;amp;tabs=check-pass&#34;&gt;manual approval&lt;/a&gt; to allow deployment to a given environment. Within the stage protected by the approval, I needed the approver&amp;rsquo;s details, specifically their email address.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I recently had the need to get the email address of the approver of a deployment to an environment from within a multi-stage YAML pipeline. Turns out it was not as easy as I might have hoped given the available documented APIs.</p>
<h3 id="background">Background</h3>
<p>My YAML pipeline included a <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;tabs=check-pass">manual approval</a> to allow deployment to a given environment. Within the stage protected by the approval, I needed the approver&rsquo;s details, specifically their email address.</p>
<p>I managed to achieve this but had to use undocumented API calls. These were discovered by looking at Azure DevOps UI operations using development tools within my browser.</p>
<h3 id="the-solution">The Solution</h3>
<p>The process was as follows</p>
<ul>
<li>Make a call to the build&rsquo;s timeline to get the current stage&rsquo;s GUID - this is documented <a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/build/timeline/get?view=azure-devops-rest-6.0">API call</a></li>
<li>Make a call to the <strong>Contribution/HierarchyQuery</strong> API to get the approver details. This is the undocumented API call.</li>
</ul>
<p>The code to do this is as shown below. It makes use of <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&amp;tabs=yaml">predefined variables</a> to pass in the details of the current run and stage.</p>
<p>Note that I had to re-create the web client object between each API call. If I did not do this I got a 400 Bad Request on the second API call - it took me ages to figure this out!</p>
<script src="https://gist.github.com/rfennell/1bc5cedf41dc169737e6bbf355f7d151.js"></script>
]]></content:encoded>
    </item>
    <item>
      <title>Fixing my SQLite Error 5: &#39;database is locked&#39; error in Entity Framework</title>
      <link>https://blog.richardfennell.net/posts/fixing-my-sqlite-error-5-database-is-locked-error-in-entity-framework/</link>
      <pubDate>Fri, 12 Feb 2021 17:21:15 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fixing-my-sqlite-error-5-database-is-locked-error-in-entity-framework/</guid>
      <description>&lt;p&gt;I have spent too long today trying to track down an intermittent “SQLite Error 5: &amp;lsquo;database is locked&amp;rsquo;” error in .Net Core Entity Framework.&lt;/p&gt;
&lt;p&gt;I have read plenty of documentation and even tried swapping to use SQL Server, as opposed to SQLite, but this just resulted in the error ‘There is already an open DataReader associated with this Connection which must be closed first.’.&lt;/p&gt;
&lt;p&gt;So everything pointed to it being a mistake I had made.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have spent too long today trying to track down an intermittent “SQLite Error 5: &lsquo;database is locked&rsquo;” error in .Net Core Entity Framework.</p>
<p>I have read plenty of documentation and even tried swapping to use SQL Server, as opposed to SQLite, but this just resulted in the error ‘There is already an open DataReader associated with this Connection which must be closed first.’.</p>
<p>So everything pointed to it being a mistake I had made.</p>
<p>And it was, it turns out the issue was I had the dbContext.SaveChanges() call inside a foreach loop</p>
<p>It was</p>
<pre tabindex="0"><code>using (var dbContext = scope.ServiceProvider.GetRequiredService()) {
    var itemsToQueue = dbContext.CopyOperations.Where(o =&gt; o.RequestedStartTime &lt; DateTime.UtcNow &amp;&amp; o.Status == OperationStatus.Queued);
    foreach (var item in itemsToQueue) {
        item.Status = OperationStatus.StartRequested;
        item.StartTime = DateTime.UtcNow;
        dbContext.SaveChanges();
    }
}
</code></pre><p>And it should have been</p>
<pre tabindex="0"><code> using (var dbContext = scope.ServiceProvider.GetRequiredService()) {
    var itemsToQueue = dbContext.CopyOperations.Where(o =&gt; o.RequestedStartTime &lt; DateTime.UtcNow &amp;&amp; o.Status == OperationStatus.Queued);
    foreach (var item in itemsToQueue) {
        item.Status = OperationStatus.StartRequested;
        item.StartTime = DateTime.UtcNow;
    }
    dbContext.SaveChanges();
}
</code></pre><p>Once this change was made my error disappeared.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What to do when moving your Azure DevOps organisation from one region to another is delayed.</title>
      <link>https://blog.richardfennell.net/posts/what-to-do-when-moving-your-azure-devops-organisation-from-one-region-to-another-is-delayed/</link>
      <pubDate>Mon, 25 Jan 2021 10:09:17 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/what-to-do-when-moving-your-azure-devops-organisation-from-one-region-to-another-is-delayed/</guid>
      <description>&lt;p&gt;There are good reasons why you might wish to move an existing Azure DevOps organisation from one region to another. The most common ones are probably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new Azure DevOps region has become available since you created your organisation that is a &amp;lsquo;better home&amp;rsquo; for your projects.&lt;/li&gt;
&lt;li&gt;New or changing national regulations require your source stored in a specific location.&lt;/li&gt;
&lt;li&gt;You want your repositories as close to your workers as possible, to reduce network latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of these reasons meant I recently had to move an Azure DevOps organisation, so followed the &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/change-organization-location?view=azure-devops#change-organization-region&#34;&gt;documented process&lt;/a&gt;. This requires you to&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>There are good reasons why you might wish to move an existing Azure DevOps organisation from one region to another. The most common ones are probably:</p>
<ul>
<li>A new Azure DevOps region has become available since you created your organisation that is a &lsquo;better home&rsquo; for your projects.</li>
<li>New or changing national regulations require your source stored in a specific location.</li>
<li>You want your repositories as close to your workers as possible, to reduce network latency.</li>
</ul>
<p>One of these reasons meant I recently had to move an Azure DevOps organisation, so followed the <a href="https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/change-organization-location?view=azure-devops#change-organization-region">documented process</a>. This requires you to</p>
<ol>
<li>Whilst logged in as the Azure DevOps organisation owner, open the <a href="https://azuredevopsvirtualagent.azurewebsites.net/">Azure DevOps Virtual Support Agent</a></li>
<li>Select the quick action &lsquo;Change Organization Region&rsquo;</li>
<li>Follow the wizard to pick the new region and the date for the move.</li>
</ol>
<p>You are warned that there could be a short loss of service during the move. Much of the move is done as a background process. It is only the final switch over that can interrupt service, hence this interruption being short.</p>
<p>I followed this process, but after the planned move date I found my organisation has not moved. In the Virtual Support Agent, I found the message.</p>
<blockquote>
<p>Please note that region move requests are currently delayed due to ongoing deployments. We may not be able to perform the change at your requested time and may ask you to reschedule. We apologize for the potential delay and appreciate your patience!</p></blockquote>
<p>I received no other emails, I suspect overly aggressive spam filters were the cause of that, but it meant I was unclear what to do next. Should I:</p>
<ol>
<li>Just wait i.e. do not reschedule anything, even though the target date is now in the past</li>
<li>Reschedule the existing move request to a date in the future using the virtual assistant wizard</li>
<li>Cancel the old request and start the process again from scratch</li>
</ol>
<p>After asking the question in the <a href="https://developercommunity2.visualstudio.com/report?entry=problem&amp;space=21">Visual Studio Developer Community Forums</a> I was told the correct action is to cancel the old request and request a new move date. It seems that once your requested date is passed the move will not take place no matter how long you wait.</p>
<p>Hence, I created a new request, which all went through exactly as planned.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Running UWP Unit Tests as part of an Azure DevOps Pipeline</title>
      <link>https://blog.richardfennell.net/posts/running-uwp-unit-tests-as-part-of-an-azure-devops-pipeline/</link>
      <pubDate>Tue, 08 Dec 2020 09:36:03 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/running-uwp-unit-tests-as-part-of-an-azure-devops-pipeline/</guid>
      <description>&lt;p&gt;I was reminded recently of the hoops you have to jump through to run &lt;a href=&#34;https://docs.microsoft.com/en-us/visualstudio/test/walkthrough-creating-and-running-unit-tests-for-windows-store-apps?view=vs-2019&#34;&gt;UWP unit tests&lt;/a&gt; within an Azure DevOps automated build.&lt;/p&gt;
&lt;p&gt;The key steps you need to remember are as follows&lt;/p&gt;
&lt;h3 id=&#34;desktop-interaction&#34;&gt;Desktop Interaction&lt;/h3&gt;
&lt;p&gt;The build agent should not be running as a service it must be able to interact with the desktop.&lt;/p&gt;
&lt;p&gt;If you did not set this mode during configuration &lt;a href=&#34;https://www.donovanbrown.com/post/auto-start-build-agent-in-interactive-mode&#34;&gt;this post from Donovan Brown&lt;/a&gt; shows how to swap the agent over without a complete reconfiguration.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I was reminded recently of the hoops you have to jump through to run <a href="https://docs.microsoft.com/en-us/visualstudio/test/walkthrough-creating-and-running-unit-tests-for-windows-store-apps?view=vs-2019">UWP unit tests</a> within an Azure DevOps automated build.</p>
<p>The key steps you need to remember are as follows</p>
<h3 id="desktop-interaction">Desktop Interaction</h3>
<p>The build agent should not be running as a service it must be able to interact with the desktop.</p>
<p>If you did not set this mode during configuration <a href="https://www.donovanbrown.com/post/auto-start-build-agent-in-interactive-mode">this post from Donovan Brown</a> shows how to swap the agent over without a complete reconfiguration.</p>
<h3 id="test-assemblies">Test Assemblies</h3>
<p>The UWP unit test projects are not built as a DLL, but as an EXE.</p>
<p>I stupidly just made my VSTest task look for the generated EXE and run the tests they contained. This does not work generating the somewhat confusing error</p>
<blockquote>
<p>Test run will use DLL(s) built for framework .NETFramework,Version=v4.0 and platform X86. Following DLL(s) do not match framework/platform settings.<br>
BlackMarble.Spectrum.FridgeManagement.Client.OneWire.UnitTests.exe is built for Framework .NETCore,Version=v5.0 and Platform X86.</p></blockquote>
<p>What you should search for as the entry point for the tests is the <strong>.appxrecipe</strong> file. Once tI used this my tests ran.</p>
<p>So my pipeline YML to run all the tests in a built solutions was</p>
<p><code>- task: VisualStudioTestPlatformInstaller@1</code><br>
<code>   inputs:</code><br>
<code>      packageFeedSelector: 'nugetOrg'</code><br>
<code>      versionSelector: 'latestPreRelease'                 </code><br>
<code>- task: VSTest@2</code><br>
<code>    displayName: 'VSTest - testAssemblies'</code><br>
<code>    inputs:</code><br>
<code>       platform: 'x86'</code><br>
<code>       configuration: '$(BuildConfiguration)'</code><br>
<code>       testSelector: 'testAssemblies' </code><br>
<code>testAssemblyVer2: | # Required when testSelector == TestAssemblies</code><br>
<code>         ***unittests.dll</code><br>
<code>        ***unittests.build.appxrecipe</code><br>
<code>         !***TestAdapter.dll</code><br>
<code>         !**obj** </code><br>
<code>       searchFolder: '$(Build.SourcesDirectory)/src'</code><br>
<code>       resultsFolder: '$(System.DefaultWorkingDirectory)TestResults'</code><br>
<code>       runInParallel: false</code><br>
<code>       codeCoverageEnabled: true</code><br>
<code>       rerunFailedTests: false</code><br>
<code>       runTestsInIsolation: true</code><br>
<code>       runOnlyImpactedTests: false</code><br>
<code>           - task: PublishTestResults@2</code><br>
<code>   displayName: 'Publish Test Results **/TEST-*.xml'</code><br>
<code>  condition: always()</code></p>
]]></content:encoded>
    </item>
    <item>
      <title>Out of Memory running SonarQube Analysis on a large projects</title>
      <link>https://blog.richardfennell.net/posts/out-of-memory-running-sonarqube-analysis-on-a-large-projects/</link>
      <pubDate>Tue, 01 Dec 2020 16:14:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/out-of-memory-running-sonarqube-analysis-on-a-large-projects/</guid>
      <description>&lt;p&gt;Whilst adding SonarQube analysis to a large project I started getting memory errors during the analysis phase. The solution was to up the memory available to the SonarQube Scanner on the my build agent, not the memory on the SonarQube server as I had first thought. This is done with an environment variable &lt;a href=&#34;https://docs.sonarqube.org/latest/analysis/scan/sonarscanner/&#34;&gt;as per the documentation&lt;/a&gt;, but how best to do this within our Azure DevOps build systems?&lt;/p&gt;
&lt;p&gt;The easiest way to set the environment variable &lt;code&gt; `SONAR_SCANNER_OPTS&lt;/code&gt;` on every build agent is to just set it via a Azure Pipeline variable. This works because the build agent makes all pipeline variables available as environment variables at runtime.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Whilst adding SonarQube analysis to a large project I started getting memory errors during the analysis phase. The solution was to up the memory available to the SonarQube Scanner on the my build agent, not the memory on the SonarQube server as I had first thought. This is done with an environment variable <a href="https://docs.sonarqube.org/latest/analysis/scan/sonarscanner/">as per the documentation</a>, but how best to do this within our Azure DevOps build systems?</p>
<p>The easiest way to set the environment variable <code> `SONAR_SCANNER_OPTS</code>` on every build agent is to just set it via a Azure Pipeline variable. This works because the build agent makes all pipeline variables available as environment variables at runtime.</p>
<p>So as I was using YML Pipeline, I set a variable within the build job</p>
<p>- `job: build<br>
timeoutInMinutes: 240<br>
variables:</p>
<ul>
<li>name: BuildConfiguration<br>
value: &lsquo;Release&rsquo;</li>
<li>name: SONAR_SCANNER_OPTS<br>
value: -Xmx4096m<br>
steps:`</li>
</ul>
<p>I found I had to quadruple the memory allocated to the scanner. Once this was done my analysis completed</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting confused over Azure DevOps Pipeline variable evaluation</title>
      <link>https://blog.richardfennell.net/posts/getting-confused-over-azure-devops-pipeline-variable-evaluation/</link>
      <pubDate>Fri, 27 Nov 2020 15:50:49 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-confused-over-azure-devops-pipeline-variable-evaluation/</guid>
      <description>&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The use of variables is important in Azure DevOps pipelines, especially when using YML templates. They allow a single pipeline to be used for multiple branches/configurations etc.&lt;/p&gt;
&lt;p&gt;The most common form of variables you see is are the &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&amp;amp;tabs=yaml&#34;&gt;predefined built in variables&lt;/a&gt; e.g. &lt;strong&gt;$(Build.BuildNumber)&lt;/strong&gt; and your own custom ones e.g. &lt;strong&gt;$(var)&lt;/strong&gt;. Usually the value of these variables are set before/as the build is run, as an input condition.&lt;/p&gt;
&lt;p&gt;But this is not the only way variables can be used. As noted in the &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&amp;amp;tabs=yaml%2Cbatch#understand-variable-syntax&#34;&gt;documentation&lt;/a&gt; there are different ways to access a variable&amp;hellip;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>The use of variables is important in Azure DevOps pipelines, especially when using YML templates. They allow a single pipeline to be used for multiple branches/configurations etc.</p>
<p>The most common form of variables you see is are the <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&amp;tabs=yaml">predefined built in variables</a> e.g. <strong>$(Build.BuildNumber)</strong> and your own custom ones e.g. <strong>$(var)</strong>. Usually the value of these variables are set before/as the build is run, as an input condition.</p>
<p>But this is not the only way variables can be used. As noted in the <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&amp;tabs=yaml%2Cbatch#understand-variable-syntax">documentation</a> there are different ways to access a variable&hellip;</p>
<blockquote>
<p>In a pipeline, template expression variables <strong>${{ variables.var }}</strong> get processed at compile time, before runtime starts. Macro syntax variables <strong>$(var)</strong> get processed during runtime before a task runs. Runtime expressions <strong>$[variables.var]</strong> also get processed during runtime but were designed for use with conditions and expressions.</p>
<p>Azure DevOps Documentation</p></blockquote>
<p>99% of the time I have been fine using just the <strong>$(var)</strong> syntax, but I recently was working on a case where this would not work for me.</p>
<h2 id="the-issue">The Issue</h2>
<p>I had a pipeline that made heavy use of <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops">YML templates</a> and <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops#conditional-insertion">conditional task insertion</a> to include sets of task based upon the manually entered and pre-defined variables.</p>
<p>The problems that one of the tasks, used in a template, set a boolean output variable <strong>$(outVar)</strong> by calling</p>
<pre tabindex="0"><code>echo &#39;##vso\[task.setvariable variable=outvar;isOutput=true\]true&#39;
</code></pre><p>This task created the output variable could be accessed by other tasks as the variable <strong>$(mytask.outvar)</strong>, but it was set at runtime it not available at the time of the YML compilation.</p>
<p>This caused me a problem as it meant that it could not be used in the template&rsquo;s <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops#conditional-insertion">conditional task inclusion</a> blocks as it as not present art compile time when this code is evaluated e.g.</p>
<pre tabindex="0"><code>\- ${{ if eq(mytask.outvar, &#39;true&#39;) }} :
  # the task to run if the condition is met
  - task: Some.Task@1 
    ....
</code></pre><p>I tied referencing the variable using all forms of $ followed by brackets syntax I could think of, but it did not help.</p>
<p>The lesson here is that you cannot make a runtime value a compile time value by wishing it to change.</p>
<p>The only solution I could find was to make use of the runtime variable in a place where it can be resolved. If you wish to enable or disable a task based on the variable value then the only option is to use the <strong>condition</strong> parameter</p>
<pre tabindex="0"><code>  # the task to run if the condition is met
  - task: Some.Task@1 
    condition: and(succeeded(), eq(mytask.outvar, &#39;true&#39;))
    ....
</code></pre><p>The only downside of this way of working as opposed to the conditional insertion is that</p>
<ul>
<li>If you conditional insertion, non required tasks are never shown in the pipeline as they are not compiled into it</li>
<li>If using the condition property to exclude a task, it will still appear in the log, but it can be seen that it has not been run.</li>
</ul>
<p>So I got there in the end, it was just not as neat as I had hoped, but I do have a clearer understanding of compile and runtime variables in Azure DevOps YML</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to export Azure DevOps Classic Builds and Release to YAML</title>
      <link>https://blog.richardfennell.net/posts/how-to-export-azure-devops-classic-builds-and-release-to-yaml/</link>
      <pubDate>Fri, 13 Nov 2020 12:03:45 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/how-to-export-azure-devops-classic-builds-and-release-to-yaml/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is another one of those posts so I can remember where some useful information is&amp;hellip;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you are migrating your &lt;a href=&#34;https://www.youtube.com/watch?v=WMQ0G9eXczE&amp;amp;t=21s&#34;&gt;Azure DevOps Classic Builds and Release to Multi-Stage YAML&lt;/a&gt; then an import step is to export all the exiting build, task groups and release as YAML files.&lt;/p&gt;
&lt;p&gt;You can do this by hand within the Pipeline UI, with a lot of cut and pasting, but much easier is to use the excellent &lt;a href=&#34;https://github.com/f2calv/yamlizr&#34;&gt;Yamlizr - Azure DevOps Classic-to-YAML Pipelines CLI&lt;/a&gt; from &lt;a href=&#34;https://github.com/f2calv&#34;&gt;Alex Vincent&lt;/a&gt;. A single CLI command exports everything with a Team project into a neat folder structure of template base YAML.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>This is another one of those posts so I can remember where some useful information is&hellip;.</em></p>
<p>If you are migrating your <a href="https://www.youtube.com/watch?v=WMQ0G9eXczE&amp;t=21s">Azure DevOps Classic Builds and Release to Multi-Stage YAML</a> then an import step is to export all the exiting build, task groups and release as YAML files.</p>
<p>You can do this by hand within the Pipeline UI, with a lot of cut and pasting, but much easier is to use the excellent <a href="https://github.com/f2calv/yamlizr">Yamlizr - Azure DevOps Classic-to-YAML Pipelines CLI</a> from <a href="https://github.com/f2calv">Alex Vincent</a>. A single CLI command exports everything with a Team project into a neat folder structure of template base YAML.</p>
<p>I cannot recommend the tool enough</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting my ThinkPad Active Pen working with my Lenovo X1 Carbon Extreme</title>
      <link>https://blog.richardfennell.net/posts/getting-my-thinkpad-active-pen-working-with-my-lenovo-x1-carbon-extreme/</link>
      <pubDate>Fri, 06 Nov 2020 15:38:02 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-my-thinkpad-active-pen-working-with-my-lenovo-x1-carbon-extreme/</guid>
      <description>&lt;p&gt;I have had a ThinkPad Active Pen (model SD60G957200) ever since I got my Lenovo X1 Carbon Extreme.&lt;/p&gt;
&lt;p&gt;The pen, when it works, has worked well. However, the problem has been that whether the pen and PC detected each other seemed very hit and miss.&lt;/p&gt;
&lt;p&gt;Today I found the root cause. It was not drivers or dodgy Bluetooth as I had thought, but a weak spring inside the pen. It was not so weak that the battery rattled, but weak enough that the electrical circuit was not being closed reliably on the battery.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have had a ThinkPad Active Pen (model SD60G957200) ever since I got my Lenovo X1 Carbon Extreme.</p>
<p>The pen, when it works, has worked well. However, the problem has been that whether the pen and PC detected each other seemed very hit and miss.</p>
<p>Today I found the root cause. It was not drivers or dodgy Bluetooth as I had thought, but a weak spring inside the pen. It was not so weak that the battery rattled, but weak enough that the electrical circuit was not being closed reliably on the battery.</p>
<p>The fix was to replace the weak spring with new one out of an old ball point pen. Once this was done the pen became instantly reliable.</p>
<p>Wish I had spotted that sooner.</p>
<p><strong>Updated 11 Nov 2020</strong>: I may have spoken too soon, it is back to it&rsquo;s old behaviour today :(</p>
<p>However, I think it could just be the AAAA battery. Seems it is not a good idea to leave a battery in when the pen is not is use given the pen has no power switch.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using GitVersion when your default branch is not called &#39;master&#39;</title>
      <link>https://blog.richardfennell.net/posts/using-gitversion-when-your-default-branch-is-not-called-master/</link>
      <pubDate>Wed, 14 Oct 2020 12:53:46 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-gitversion-when-your-default-branch-is-not-called-master/</guid>
      <description>&lt;p&gt;The Black Live Matter movement has engendered many conversations, hopefully starting changes for the good. Often these changes involve the use of language. One such change has been the move to stop using the name &lt;code&gt;master&lt;/code&gt; and switching to the name &lt;code&gt;main&lt;/code&gt; for the trunk/default branch in Git repos. This change is moving apace driven by tools such as &lt;a href=&#34;https://github.com/github/renaming&#34;&gt;GitHub&lt;/a&gt; and &lt;a href=&#34;https://devblogs.microsoft.com/devops/azure-repos-default-branch-name/&#34;&gt;Azure DevOps&lt;/a&gt; .&lt;/p&gt;
&lt;p&gt;I have recently had need, for the first time since swapping my default branch name in new repos to &lt;code&gt;main&lt;/code&gt;, to use Semantic Version and the GitVersion tool.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The Black Live Matter movement has engendered many conversations, hopefully starting changes for the good. Often these changes involve the use of language. One such change has been the move to stop using the name <code>master</code> and switching to the name <code>main</code> for the trunk/default branch in Git repos. This change is moving apace driven by tools such as <a href="https://github.com/github/renaming">GitHub</a> and <a href="https://devblogs.microsoft.com/devops/azure-repos-default-branch-name/">Azure DevOps</a> .</p>
<p>I have recently had need, for the first time since swapping my default branch name in new repos to <code>main</code>, to use Semantic Version and the GitVersion tool.</p>
<p>&lsquo;Out of the box&rsquo; I hit a problem. The current shipping version of <a href="https://gitversion.net/docs/">GitVersion</a> (5.3.2) by default makes the assumption that&rsquo;s the trunk branch is called <code>master</code>. Hence, throws an exception if this branch cannot be found.</p>
<p>Looking at the <a href="https://github.com/GitTools/GitVersion">project&rsquo;s repo</a> you can find PRs, tagged for a future release, that address this constraint. However, you don&rsquo;t have to wait for a new version to ship to use this excellent tool in repos with other branch naming conventions.</p>
<p>The solution is to create an override file <code>GitVersion.yml</code> in the root of your repo with the following content to alter the Regex used to find branches. Note that the content below is as a minimum, you can override any other default <a href="https://gitversion.net/docs/configuration">configuration</a> values in this file as needed.</p>
<pre tabindex="0"><code>branches:  
   master:  
      regex: ^master$|^main$
</code></pre><p>With this override file the default branch can be either <code>master</code> or <code>main</code>.</p>
<p>You can of course use a different name or limit the Regex to a single name as you need.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How do handle PRs for Azure DevOps YAML Pipelines if the YAML templates are in a different repo?</title>
      <link>https://blog.richardfennell.net/posts/how-do-handle-prs-for-azure-devops-yaml-pipelines-if-the-yaml-templates-are-in-a-different-repo/</link>
      <pubDate>Fri, 18 Sep 2020 12:11:24 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/how-do-handle-prs-for-azure-devops-yaml-pipelines-if-the-yaml-templates-are-in-a-different-repo/</guid>
      <description>&lt;p&gt;Azure DevOps YAML base pipelines allow the pipeline definitions to be treated like any other code. So you make changes in a branch and PR them into the main/trunk when they are approved.&lt;/p&gt;
&lt;p&gt;This works well if all the YAML files are in the same repo, but not so well if you are using &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops&#34;&gt;YAML templates&lt;/a&gt; and the templated YAML is stored in a different repo. This is because an Azure DevOps PR is limited to a single repo. So testing a change to a YAML template in a different repo needs a bit of thought.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Azure DevOps YAML base pipelines allow the pipeline definitions to be treated like any other code. So you make changes in a branch and PR them into the main/trunk when they are approved.</p>
<p>This works well if all the YAML files are in the same repo, but not so well if you are using <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops">YAML templates</a> and the templated YAML is stored in a different repo. This is because an Azure DevOps PR is limited to a single repo. So testing a change to a YAML template in a different repo needs a bit of thought.</p>
<p>Say for example you have a template called <strong>core.yml</strong> in a repo called <strong>YAMLTemplates</strong> and you make a change to it and start a PR. Unless you have a test YAML pipeline in that repo, which is not a stupid idea, but not always possible depending on the complexity of your build process, there is no way to test the change inside that repo.</p>
<p>The answer is to create a temporary branch in a repo that consumes the shared YAML template. In this temporary branch make an edit to the repository setting that references the shared YAML repo to point to the update branch contain the PR</p>
<p><code>resources: </code><br>
<code>repositories:</code><br>
<code>  - repository: YAMLTemplates</code><br>
<code>   type: git</code><br>
<code>   name: 'Git Project/YAMLTemplates'</code><br>
# <code>defaults to ref: 'refs/heads/master'</code><br>
<code>ref: 'refs/heads/newbranch'</code></p>
<p>You don&rsquo;t need to make any change to the line where the template is used</p>
<p><code>extends:  </code><br>
<code>template: core.yml@YAMLTemplates</code><br>
<code>  parameters:</code><br>
<code>    customer: ${{parameters.Customer}}</code><br>
<code>    useSonarQube: ${{parameters.useSonarQube}}</code></p>
<p>You can then use this updated pipeline to validated your PR. Once you are happy it works you can</p>
<ol>
<li>Complete the PR in the YAML Templates repo</li>
<li>Delete the temporary branch in your consuming repo.</li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>How can I automatically create Azure DevOps Release Notes and how can I publish them?</title>
      <link>https://blog.richardfennell.net/posts/how-can-i-automatically-create-azure-devops-release-notes-and-how-can-i-publish-them/</link>
      <pubDate>Tue, 15 Sep 2020 14:28:50 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/how-can-i-automatically-create-azure-devops-release-notes-and-how-can-i-publish-them/</guid>
      <description>&lt;p&gt;A question I am often asked when consulting on Azure DevOps is ‘how can I automatically create release notes and how can I publish them?’.&lt;/p&gt;
&lt;p&gt;Well it is for just this requirement that I have written a set of Azure DevOps Pipeline Tasks&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes&#34;&gt;Release Note Generator&lt;/a&gt; - to generate release notes. I strongly recommend this Cross-platform Node-based version. I plan to deprecate my older PowerShell version in the not too distant future as it uses ‘homegrown logic’, as opposed to standard Azure DevOps API calls, to get associated items.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WIKIUpdater-Tasks&#34;&gt;Wiki Updater&lt;/a&gt; - to upload a page tot a WIKI.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WikiPDFExport-Tasks&#34;&gt;WIKI PDF Generator&lt;/a&gt; - to convert a generated page, or whole WIKI, to PDF format.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So lets deal with these tools in turn&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A question I am often asked when consulting on Azure DevOps is ‘how can I automatically create release notes and how can I publish them?’.</p>
<p>Well it is for just this requirement that I have written a set of Azure DevOps Pipeline Tasks</p>
<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes">Release Note Generator</a> - to generate release notes. I strongly recommend this Cross-platform Node-based version. I plan to deprecate my older PowerShell version in the not too distant future as it uses ‘homegrown logic’, as opposed to standard Azure DevOps API calls, to get associated items.</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WIKIUpdater-Tasks">Wiki Updater</a> - to upload a page tot a WIKI.</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WikiPDFExport-Tasks">WIKI PDF Generator</a> - to convert a generated page, or whole WIKI, to PDF format.</li>
</ul>
<p>So lets deal with these tools in turn</p>
<h3 id="generating-release-notes">Generating Release Notes</h3>
<p>The Release Note task generates release notes by getting the items associated with a build (or release) from the Azure DevOps API and generating a document based on a <a href="https://handlebarsjs.com/">Handlebars</a> based template.</p>
<ul>
<li>The artefacts that can be included in the release notes are details of the build/release and associated Work Items, Commits/Changesets, Tests and Pull Requests.</li>
<li>Most of the <a href="https://github.com/rfennell/AzurePipelines/tree/master/SampleTemplates/XplatGenerateReleaseNotes%20%28Node%20based%29/Version%203">sample templates provided</a> are for markdown format files. However, they could easily be converted for other text-based formats such as HTML if needed.</li>
<li>The use of Handlebars are the templating language makes for a very flexible and easily extensible means of document generation. There are <a href="https://github.com/rfennell/AzurePipelines/tree/master/SampleTemplates/XplatGenerateReleaseNotes%20%28Node%20based%29/Version%203">sample of custom extensions provided with the templates</a></li>
</ul>
<p>Sample YAML for this task is as follows, not it is using an inline template but it is possible to also load the template from a file path</p>
<pre tabindex="0"><code> - task: richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes.XplatGenerate-Release-Notes.XplatGenerateReleaseNotes@3
          displayName: &#39;Generate Release Notes&#39;
          inputs:
            outputfile: &#39;$(System.DefaultWorkingDirectory)inline.md&#39;
            outputVariableName: OutputText
            templateLocation: InLine
            inlinetemplate: |
              # Notes for build 
              **Build Number**: {{buildDetails.id}}
              **Build Trigger PR Number**: {{lookup buildDetails.triggerInfo &#39;pr.number&#39;}} 

              # Associated Pull Requests ({{pullRequests.length}})
              {{#forEach pullRequests}}
              {{#if isFirst}}### Associated Pull Requests (only shown if  PR) {{/if}}
              *  **PR {{this.id}}**  {{this.title}}
              {{/forEach}}

              # Builds with associated WI/CS ({{builds.length}})
              {{#forEach builds}}
              {{#if isFirst}}## Builds {{/if}}
              ##  Build {{this.build.buildNumber}}
              {{#forEach this.commits}}
              {{#if isFirst}}### Commits {{/if}}
              - CS {{this.id}}
              {{/forEach}}
              {{#forEach this.workitems}}
              {{#if isFirst}}### Workitems {{/if}}
              - WI {{this.id}}
              {{/forEach}} 
              {{/forEach}}

              # Global list of WI ({{workItems.length}})
              {{#forEach workItems}}
              {{#if isFirst}}## Associated Work Items (only shown if  WI) {{/if}}
              *  **{{this.id}}**  {{lookup this.fields &#39;System.Title&#39;}}
                - **WIT** {{lookup this.fields &#39;System.WorkItemType&#39;}} 
                - **Tags** {{lookup this.fields &#39;System.Tags&#39;}}
              {{/forEach}}

              {{#forEach commits}}
              {{#if isFirst}}### Associated commits{{/if}}
              * ** ID{{this.id}}** 
                -  **Message:** {{this.message}}
                -  **Commited by:** {{this.author.displayName}} 
                -  **FileCount:** {{this.changes.length}} 
              {{#forEach this.changes}}
                    -  **File path (TFVC or TfsGit):** {{this.item.path}}  
                    -  **File filename (GitHub):** {{this.filename}}  
              {{/forEach}}
              {{/forEach}}
</code></pre><h3 id="how-to-publish-the-notes">How to Publish The Notes</h3>
<p>Once the document has been generated there is a need for a decision as to how to publish it. TThere are a few options</p>
<ul>
<li>Attach the markdown file as an artefact to the Build or Pipeline. Note you can’t do this with a UI based Releases as they have no concept of artefacts, but this is becoming less of a concern as people move to multistage YAML.</li>
<li>Save in some other location e.g Azure Storage or if on-premises a UNC file share</li>
<li>Send the document as an email – I have used <a href="https://marketplace.visualstudio.com/items?itemName=rvo.SendEmailTask">Rene van Osnabrugge Send Email Task</a> for this job.</li>
<li>Upload it to a WIKI using my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WIKIUpdater-Tasks">WIKI Updater Task</a></li>
<li>Convert the markdown release note document, or the whole WIKI, to a PDF and use any of the above options using first my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WikiPDFExport-Tasks">WIKI PDF Exporter Task</a> then another task.</li>
</ul>
<p>I personally favour the 1st and 4th options used together. Attachment to the pipeline and then upload the document to a WIKI</p>
<p>A sample of suitable YAML is shown below, uploading the document to an Azure DevOps WIKI. Please note that the repo URL and authentication can trip you up here so <a href="https://github.com/rfennell/AzurePipelines/wiki/WIKI-Updater-Tasks">have a good read of the provided documentation</a> before you use this task.</p>
<pre tabindex="0"><code> - task: richardfennellBM.BM-VSTS-WIKIUpdater-Tasks.WikiUpdaterTask.WikiUpdaterTask@1
          displayName: &#39;Git based WIKI Updater&#39;
          inputs:
            repo: &#39;dev.azure.com/richardfennell/Git%20project/_git/Git-project.wiki&#39;
            filename: &#39;xPlatReleaseNotes/build-Windows-handlebars.md&#39;
            dataIsFile: true
            sourceFile: &#39;$(System.DefaultWorkingDirectory)inline.md&#39;
            message: &#39;Update from Build&#39;
            gitname: builduser
            gitemail: &#39;build@demo&#39;
            useAgentToken: true
</code></pre><h3 id="but-when-do-i-generate-the-release-notes">But when do I generate the release notes?</h3>
<p>I would suggest you always generate release notes every build/pipeline i.e. a document of the changes since the last successful build/pipeline of that build definition. This should be attached as an artefact.</p>
<p>However, this per build document will usually too granular for use as ‘true’ release notes i.e. something to hand to a QA team, auditor or client.</p>
<p>To address this second use case I suggest, within a multistage YAML pipeline (or a UI based release), having a stage specifically for generating release notes.</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2020/09/image-1-1024x298.png"></p>
<p>My task has a feature that it will check for the last successful release of a pipeline/release to the stage it is defined in, so will base the release note on the last successful release to that given stage. If this &lsquo;documentation&rsquo; stage is only run when you are doing a ‘formal’ release, the release note generated will be since the last formal release. Exactly what a QA team or auditor or client might want.</p>
<h3 id="in-conclusion">In conclusion</h3>
<p>So I hope that this post provides some ideas as to how you can use my tasks generate some useful release notes.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fix For: &amp;lsquo;The pipeline is not valid error: Unable to resolve latest version&amp;rsquo; on an Azure DevOps YAML pipeline</title>
      <link>https://blog.richardfennell.net/posts/fix-for-the-pipeline-is-not-valid-error-unable-to-resolve-latest-version-on-an-azure-devops-yaml-pipeline/</link>
      <pubDate>Thu, 27 Aug 2020 15:54:52 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fix-for-the-pipeline-is-not-valid-error-unable-to-resolve-latest-version-on-an-azure-devops-yaml-pipeline/</guid>
      <description>&lt;h3 id=&#34;the-issue&#34;&gt;The Issue&lt;/h3&gt;
&lt;p&gt;I have an Azure DevOps multi-stage YAML pipeline that started giving the error `The pipeline is not valid error: Unable to resolve latest version for pipeline templates: this could be due to inaccessible pipeline or no version is available` and failing instantly.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/08/image-1.png&#34;&gt;&lt;img alt=&#34;image&#34; loading=&#34;lazy&#34; src=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/08/image_thumb-1.png&#34; title=&#34;image&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;the-solution&#34;&gt;The Solution&lt;/h3&gt;
&lt;p&gt;This is not the most helpful message, but after some digging I found the problem.&lt;/p&gt;
&lt;p&gt;The pipeline used another pipeline as a resources&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="the-issue">The Issue</h3>
<p>I have an Azure DevOps multi-stage YAML pipeline that started giving the error `The pipeline is not valid error: Unable to resolve latest version for pipeline templates: this could be due to inaccessible pipeline or no version is available` and failing instantly.</p>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/08/image-1.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/08/image_thumb-1.png" title="image"></a></p>
<h3 id="the-solution">The Solution</h3>
<p>This is not the most helpful message, but after some digging I found the problem.</p>
<p>The pipeline used another pipeline as a resources</p>
<pre tabindex="0"><code>resources:   
  pipelines:  
  - pipeline: templates  
    source: QueuesAndFunctionsDemo-CI    
    branch: master
</code></pre><p>This referenced build had failed, so there was no successful build resources to load, hence the error.</p>
<p>Once the problem with this reference build was fixed the error message went away and I could trigger my build</p>
]]></content:encoded>
    </item>
    <item>
      <title>Exporting Release Notes and WIKIs as PDFs using a new Azure DevOps Extension that wrappers AzureDevOps.WikiPDFExport</title>
      <link>https://blog.richardfennell.net/posts/exporting-release-notes-and-wikis-as-pdfs-using-a-new-azure-devops-extension-that-wrappers-azuredevops-wikipdfexport/</link>
      <pubDate>Thu, 27 Aug 2020 10:17:12 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/exporting-release-notes-and-wikis-as-pdfs-using-a-new-azure-devops-extension-that-wrappers-azuredevops-wikipdfexport/</guid>
      <description>&lt;p&gt;A common question I get when people are using my &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes&#34;&gt;Release Notes task for Azure DevOps&lt;/a&gt; is whether it is possible to get the release notes as a PDF. In the past, the answer was that I did not know of any easy way. However, I have recently come across a command line tool by Max Melcher called &lt;a href=&#34;https://github.com/MaxMelcher/AzureDevOps.WikiPDFExport&#34;&gt;AzureDevOps.WikiPDFExport&lt;/a&gt; that allows you to export a whole WIKI (or a single file) as a PDF. Its basic usage is&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A common question I get when people are using my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-XplatGenerateReleaseNotes">Release Notes task for Azure DevOps</a> is whether it is possible to get the release notes as a PDF. In the past, the answer was that I did not know of any easy way. However, I have recently come across a command line tool by Max Melcher called <a href="https://github.com/MaxMelcher/AzureDevOps.WikiPDFExport">AzureDevOps.WikiPDFExport</a> that allows you to export a whole WIKI (or a single file) as a PDF. Its basic usage is</p>
<ul>
<li>Clone a WIKI Repo</li>
<li>Run the command line tool passing in a path to the root of the cloned repo</li>
<li>The .order file is read</li>
<li>A PDF is generated</li>
</ul>
<p>This is a nice and simple process, but it would be nice to be able to automate this process as part of a build pipeline. After a bit of thought, I realised I had much of the code I needed to automated the process in my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WIKIUpdater-Tasks">WIKIUpdater extension</a> as these tasks are based around cloning repos. So I am please to say I have just released a <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WikiPDFExport-Tasks">new Azure DevOps extension WikiPDFExport</a> that wrappers Max’s command line tool. It does the following</p>
<ul>
<li>Downloads the latest release of the WikiPDFExport tool from GitHub to the build agent (the exe is too big to include in the VSIX package)</li>
<li>Optionally clone a Git based WIKI repo. As with my WikIUpdater tasks, you can pass credentials for Azure DevOps or GitHub</li>
<li>Generate a PDF of a single file or the whole of a Wiki folder structure (based on the .order file) that was either cloned or was already present on the agent</li>
</ul>
<p>A sample of the YAML usage of the task is as shown below. For full documentation see the extensions wiki pages for <a href="https://github.com/rfennell/AzurePipelines/wiki/WIKI-PdfExport-Task">general usage and troubleshooting</a> and the full <a href="https://github.com/rfennell/AzurePipelines/wiki/WIKI-PdfExport-Task-YAML">YAML specification</a>```
- task: <a href="mailto:richardfennellBM.BM-VSTS-WikiPDFExport-Tasks.WikiPDFExportTask.WikiPdfExportTask@1">richardfennellBM.BM-VSTS-WikiPDFExport-Tasks.WikiPDFExportTask.WikiPdfExportTask@1</a>
displayName: &lsquo;Export Single File generated by the release notes task&rsquo;
inputs:
cloneRepo: false
localpath: &lsquo;$(System.DefaultWorkingDirectory)&rsquo;
singleFile: &lsquo;inline.md&rsquo;
outputFile: &lsquo;$(Build.ArtifactStagingDirectory)PDFsingleFile.pdf&rsquo;</p>
<ul>
<li>task: <a href="mailto:richardfennellBM.BM-VSTS-WikiPDFExport-Tasks.WikiPDFExportTask.WikiPdfExportTask@1">richardfennellBM.BM-VSTS-WikiPDFExport-Tasks.WikiPDFExportTask.WikiPdfExportTask@1</a>
displayName: &lsquo;Export a public GitHub WIKI&rsquo;
inputs:
cloneRepo: true
repo: &lsquo;<a href="https://github.com/rfennell/AzurePipelines.wiki.git%27">https://github.com/rfennell/AzurePipelines.wiki.git'</a>
useAgentToken: false
localpath: &lsquo;$(System.DefaultWorkingDirectory)GitHubRepo&rsquo;
outputFile: &lsquo;$(Build.ArtifactStagingDirectory)PDFpublicGitHub.pdf&rsquo;</li>
<li>task: <a href="mailto:richardfennellBM.BM-VSTS-WikiPDFExport-Tasks.WikiPDFExportTask.WikiPdfExportTask@1">richardfennellBM.BM-VSTS-WikiPDFExport-Tasks.WikiPDFExportTask.WikiPdfExportTask@1</a>
displayName: &lsquo;Export a private Azure DevOps WIKI&rsquo;
inputs:
cloneRepo: true
repo: &lsquo;<a href="https://dev.azure.com/richardfennell/GitHub/">https://dev.azure.com/richardfennell/GitHub/</a>_git/GitHub.wiki&rsquo;
useAgentToken: true
localpath: &lsquo;$(System.DefaultWorkingDirectory)AzRepo&rsquo;
outputFile: &lsquo;$(Build.ArtifactStagingDirectory)PDFAzrepo.pdf&rsquo;</li>
</ul>
<pre tabindex="0"><code class="language-So" data-lang="So"></code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Using the Post Build Cleanup Task from the Marketplace in YAML based Azure DevOps Pipelines</title>
      <link>https://blog.richardfennell.net/posts/using-the-post-build-cleanup-task-from-the-marketplace-in-yaml-based-azure-devops-pipelines/</link>
      <pubDate>Wed, 19 Aug 2020 14:58:39 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-the-post-build-cleanup-task-from-the-marketplace-in-yaml-based-azure-devops-pipelines/</guid>
      <description>&lt;p&gt;Disks filling up on our private Azure DevOps agents is a constant battle. We have maintenance jobs setup on the agent pools, to clean out old build working folders nightly, but these don’t run often enough. We need a clean out more than once a day due to the number and size of our builds. To address this, with UI based builds, we successfully used the &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=mspremier.PostBuildCleanup&#34;&gt;Post Build Cleanup Extension&lt;/a&gt;. However since we have moved many of our builds to YAML we found it not working so well. Turned out the problem was due to the way got source code. The Post Build Cleanup task is intelligent, it does not just delete folders on demand. It check to see what the Get Source ‘Clean’ setting was when the repo was cloned and bases what it deletes on this value e.g. nothing, source, or everything. This behaviour is not that obvious. In a UI based builds it is easy to check this setting. You are always in the UI when editing the build. However, in YAML it is easy to forget the setting, as it is one of those few values that cannot be set in YAML. To make the post build cleanup task actually delete folders in a YAML pipeline you need to&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Disks filling up on our private Azure DevOps agents is a constant battle. We have maintenance jobs setup on the agent pools, to clean out old build working folders nightly, but these don’t run often enough. We need a clean out more than once a day due to the number and size of our builds. To address this, with UI based builds, we successfully used the <a href="https://marketplace.visualstudio.com/items?itemName=mspremier.PostBuildCleanup">Post Build Cleanup Extension</a>. However since we have moved many of our builds to YAML we found it not working so well. Turned out the problem was due to the way got source code. The Post Build Cleanup task is intelligent, it does not just delete folders on demand. It check to see what the Get Source ‘Clean’ setting was when the repo was cloned and bases what it deletes on this value e.g. nothing, source, or everything. This behaviour is not that obvious. In a UI based builds it is easy to check this setting. You are always in the UI when editing the build. However, in YAML it is easy to forget the setting, as it is one of those few values that cannot be set in YAML. To make the post build cleanup task actually delete folders in a YAML pipeline you need to</p>
<ol>
<li>Edit the pipeline</li>
<li>Click the ellipse menu top right</li>
<li>Pick Triggers</li>
<li>Pick YAML and select the ‘Get Source’ block</li>
<li>Make sure the ‘Clean’ setting is set to ‘true’ and the right set of items to delete are selected <em>– if this is not done the post clean up task does nothing</em><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/08/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/08/image_thumb.png" title="image"></a></li>
<li>You can then add the post build cleanup task the end of the steps</li>
</ol>
<pre tabindex="0"><code>steps:
  - script: echo This where you do stuff
  - task: mspremier.PostBuildCleanup.PostBuildCleanup-task.PostBuildCleanup@3
    displayName: &#39;Clean Agent Directories&#39;
    condition: always()


```Once this is done it behaves as expected
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Zwift and the joys of home networking</title>
      <link>https://blog.richardfennell.net/posts/zwift-and-the-joys-of-home-networking/</link>
      <pubDate>Mon, 13 Jul 2020 09:10:55 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/zwift-and-the-joys-of-home-networking/</guid>
      <description>&lt;p&gt;During the Covid 19 lock down I have been doing plenty of &lt;a href=&#34;https://zwift.com/feed?utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=zwift_eur_uk_cycling_search_brandcore_performance_eng-imprshare-20&amp;amp;gclid=CjwKCAjwjLD4BRAiEiwAg5NBFkOs6pvK2l4x_VOwElyHdtZaocpSugxuU4wq5nOLyeWLDw8sPesOHRoCvjgQAvD_BwE&#34;&gt;Zwift&lt;/a&gt;&amp;lsquo;ing. However, I have started having problems getting the &lt;a href=&#34;https://zwift.com/companion?utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=zwift_eur_uk_cycling_search_brand_performance_eng-imprshare-20&amp;amp;gclid=CjwKCAjwjLD4BRAiEiwAg5NBFs0iBpEj-wITNCD0VowmooCQMHQExt5JjRs3Ff0uV4ZZ8DQzQTWaNxoC76cQAvD_BwE&#34;&gt;Zwift Companion App&lt;/a&gt; working reliably, when it used to work.&lt;/p&gt;
&lt;p&gt;Basically, Zwift itself was fine, though very slow to save when exiting, but the companion app could not seem to detect that I was actively Zwift&amp;rsquo;ing, but it&amp;rsquo;s other functions were OK.&lt;/p&gt;
&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/wp-content/uploads/sites/2/2020/07/uVSoNkxbVgjS78uvjUY30M8OMG5JdiHogtQjZzmsAWE-2048x1121-1-1024x561.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;After much fiddling I found the issue was the network connection from my PC up to Zwift and nothing to do with the phone app. But in case it is of any use to others here are the steps I took to&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>During the Covid 19 lock down I have been doing plenty of <a href="https://zwift.com/feed?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=zwift_eur_uk_cycling_search_brandcore_performance_eng-imprshare-20&amp;gclid=CjwKCAjwjLD4BRAiEiwAg5NBFkOs6pvK2l4x_VOwElyHdtZaocpSugxuU4wq5nOLyeWLDw8sPesOHRoCvjgQAvD_BwE">Zwift</a>&lsquo;ing. However, I have started having problems getting the <a href="https://zwift.com/companion?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=zwift_eur_uk_cycling_search_brand_performance_eng-imprshare-20&amp;gclid=CjwKCAjwjLD4BRAiEiwAg5NBFs0iBpEj-wITNCD0VowmooCQMHQExt5JjRs3Ff0uV4ZZ8DQzQTWaNxoC76cQAvD_BwE">Zwift Companion App</a> working reliably, when it used to work.</p>
<p>Basically, Zwift itself was fine, though very slow to save when exiting, but the companion app could not seem to detect that I was actively Zwift&rsquo;ing, but it&rsquo;s other functions were OK.</p>
<p><img loading="lazy" src="/wp-content/uploads/sites/2/2020/07/uVSoNkxbVgjS78uvjUY30M8OMG5JdiHogtQjZzmsAWE-2048x1121-1-1024x561.jpg"></p>
<p>After much fiddling I found the issue was the network connection from my PC up to Zwift and nothing to do with the phone app. But in case it is of any use to others here are the steps I took to</p>
<ul>
<li>Ran a <a href="https://play.google.com/store/apps/details?id=com.farproc.wifi.analyzer&amp;hl=en_GB">WiFi network analysis app</a> and realised that
<ul>
<li>My local wireless environment is now very congested, I assume as more people are working from home.</li>
<li>Both the 2.4GHz and 5Ghz network were on the same channels as other strong signals.</li>
<li>Also they were using the same SSID, which is meant to provide seamless swap-over between 2.4 and 5Ghz. But, in reality this meant there were connection problems as a connection flipped between frequencies.</li>
</ul>
</li>
</ul>
<p>This explained other problems I had seem</p>
<ul>
<li>The <a href="https://docs.microsoft.com/en-us/windows-server/remote/remote-access/directaccess/directaccess">Microsoft Direct Access</a> VPN I use to connect to the office failing intermittently. <em>Obviously, any problems I have connecting to the office to do work is far less important than Zwift connection issues.</em></li>
<li>My Samsung phone would drop calls for no reason. I now think this was when it had decided to use Wifi calling and got confused over networks.<br>
<strong>Note:</strong> I had fixed this by switching off Wifi calling.</li>
</ul>
<p>To address the problems I changed the SSIDs so that my 2.4 and 5Ghz networks had different names, so that I know which one I was using. Also I moved the channels to ones not used by my neighbours</p>
<p><strong>Test</strong></p>
<p><strong>Result</strong></p>
<p>Put the phone and the PC on the 2.4Ghz network</p>
<p>No improvement, app did not work and PC slow to save</p>
<p>Put the phone and the PC on 5Ghz</p>
<p>Small improvement, app still did not work but at least tried to show the in game view before it dropped out. The PC was still slow to save</p>
<p>Put the phone on either Wifi network but the the PC on <a href="https://www.cclonline.com/product/210080/TL-PA4010P-KIT-V2-20/Mains-Networking/TP-LINK-AV600-TL-PA4010P-600Mbps-Passthrough-Powerline-Starter-Kit-Twin-Pack-V2-2-/NET2499/?gclid=CjwKCAjwjLD4BRAiEiwAg5NBFpqSdEjxVtvq47Esz7JQ-skCQDZsMDx91bcKW0Gh-ktJZz_nznds8BoCMn8QAvD_BwE">Ethernet over Power using TPLink adaptors</a></p>
<p>This fixed it</p>
<p>So it seems the problem was upload speed from my PC all along. Strange as I would have expected the 5Ghz network to be fine, even if the 2.4Ghz was not. The 5Ghz Wifi seems to perform OK on a speed test.</p>
<p>Anyway it is working now, but maybe it is time to consider a proper mesh network?</p>
]]></content:encoded>
    </item>
    <item>
      <title>Timeout Errors &#39;Extracting Schema&#39; when running SQLPackage for a Migration to Azure DevOps Services</title>
      <link>https://blog.richardfennell.net/posts/timeout-errors-extracting-schema-when-running-sqlpackage-for-a-migration-to-azure-devops-services/</link>
      <pubDate>Thu, 25 Jun 2020 09:39:25 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/timeout-errors-extracting-schema-when-running-sqlpackage-for-a-migration-to-azure-devops-services/</guid>
      <description>&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Whilst doing a migration from an on-premised TFS to Azure DevOps Services for a client I had a strange issue with SQLPackage.exe.&lt;/p&gt;
&lt;p&gt;I had previously completed the dry run of the migration without any issues and started the live migration with a fully defined process and timings for a each stage.&lt;/p&gt;
&lt;p&gt;When I came to export the detached Team Project Collection DB I ran the same command as I had for the dry run&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="the-problem">The Problem</h3>
<p>Whilst doing a migration from an on-premised TFS to Azure DevOps Services for a client I had a strange issue with SQLPackage.exe.</p>
<p>I had previously completed the dry run of the migration without any issues and started the live migration with a fully defined process and timings for a each stage.</p>
<p>When I came to export the detached Team Project Collection DB I ran the same command as I had for the dry run</p>
<pre tabindex="0"><code>&amp; &#34;C:Program FilesMicrosoft SQL Server150DACbinSqlPackage.exe&#34; /sourceconnectionstring:”Data Source=localhostSQLExpress;Initial Catalog=Tfs\_DefaultCollection;Integrated Security=True” /targetFile:C:tempTfs\_DefaultCollection.dacpac /action:extract /p:ExtractAllTableData=true /p:IgnoreUserLoginMappings=true /p:IgnorePermissions=true /p:Storage=Memory 
</code></pre><p>I had expected this to take around 30 minutes. However it failed after 10 minutes with an error when trying to export the schema &lsquo;Timeout, cannot reconnect to the database&rsquo;.</p>
<p>This was strange as nothing had changed on the system since the dry-run. I tried all of the following with no effect</p>
<ul>
<li>Just running the command again, you can hope!</li>
<li>Restarted SQL and ran the command again</li>
<li>Tried the export from SQL Management Studio as opposed to the command line , this just seems to hang at the same point.</li>
</ul>
<h3 id="the-solution">The Solution</h3>
<p>What resolved the problem was a complete reboot of the virtual machine. I assume the issue was some locked resource, but not idea why.</p>
<hr>
<h3 id="updated-29th-july-2020">Updated 29th July 2020</h3>
<p>I had the same problem with another client upgrade. This time a reboot did not fix it.</p>
<p>The solution at this site was to upgrade SQLPackage from the 140 32bit version to the 64bit 150 version. Once this was done the command ran without a problem</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fix for &amp;lsquo;System.BadImageFormatException&amp;rsquo; when running x64 based tests inside a Azure DevOps Release</title>
      <link>https://blog.richardfennell.net/posts/fix-for-system-badimageformatexception-when-running-x64-based-tests-inside-a-azure-devops-release/</link>
      <pubDate>Thu, 23 Apr 2020 10:05:35 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fix-for-system-badimageformatexception-when-running-x64-based-tests-inside-a-azure-devops-release/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is one of those blog posts I write to remind my future self how I fixed a problem.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I have a release that installs VSTest and runs some integration tests that target .NET 4.6 x64. All these tests worked fine in Visual Studio. However, I got the following errors for all tests when they were run in a release&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;2020-04-23T09:30:38.7544708Z vstest.console.exe &amp;#34;C:agent\_workr1aPaymentServicesdroptestartifactsPaymentService.IntegrationTests.dll&amp;#34;

2020-04-23T09:30:38.7545688Z /Settings:&amp;#34;C:agent\_work\_tempuxykzf03ik2.tmp.runsettings&amp;#34;

2020-04-23T09:30:38.7545808Z /Logger:&amp;#34;trx&amp;#34;

2020-04-23T09:30:38.7545937Z /TestAdapterPath:&amp;#34;C:agent\_workr1aPaymentServicesdroptestartifacts&amp;#34;

2020-04-23T09:30:39.2634578Z Starting test execution, please wait...

2020-04-23T09:30:39.4783658Z A total of 1 test files matched the specified pattern.

2020-04-23T09:30:40.8660112Z   X Can\_Get\_MIDs \[521ms\]

2020-04-23T09:30:40.8684249Z   Error Message:

2020-04-23T09:30:40.8684441Z    Test method PaymentServices.IntegrationTests.ControllerMIDTests.Can\_Get\_MIDs threw exception: 

2020-04-23T09:30:40.8684574Z System.BadImageFormatException: Could not load file or assembly &amp;#39;PaymentServices, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null&amp;#39; or one of its dependencies. An attempt was made to load a program with an incorrect format.

2020-04-23T09:30:40.8684766Z   Stack Trace:

2020-04-23T09:30:40.8684881Z       at PaymentServices.IntegrationTests.ControllerMIDTests.Can\_Get\_MIDs()

…

2020-04-23T09:30:40.9038788Z Results File: C:agent\_work\_tempTestResultssvc-devops\_SVRHQAPP027\_2020-04-23\_10\_30\_40.trx

2020-04-23T09:30:40.9080344Z Total tests: 22

2020-04-23T09:30:40.9082348Z      Failed: 22

2020-04-23T09:30:40.9134858Z ##\[error\]Test Run Failed.
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;solution&#34;&gt;Solution&lt;/h3&gt;
&lt;p&gt;I needed to tell &lt;strong&gt;vstest.console.exe&lt;/strong&gt; to run x64 as opposed to it’s default of x32. This can be done with a command line override &lt;strong&gt;–platform:x64&lt;/strong&gt;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>This is one of those blog posts I write to remind my future self how I fixed a problem.</em></p>
<h3 id="the-problem">The Problem</h3>
<p>I have a release that installs VSTest and runs some integration tests that target .NET 4.6 x64. All these tests worked fine in Visual Studio. However, I got the following errors for all tests when they were run in a release</p>
<pre tabindex="0"><code>2020-04-23T09:30:38.7544708Z vstest.console.exe &#34;C:agent\_workr1aPaymentServicesdroptestartifactsPaymentService.IntegrationTests.dll&#34;

2020-04-23T09:30:38.7545688Z /Settings:&#34;C:agent\_work\_tempuxykzf03ik2.tmp.runsettings&#34;

2020-04-23T09:30:38.7545808Z /Logger:&#34;trx&#34;

2020-04-23T09:30:38.7545937Z /TestAdapterPath:&#34;C:agent\_workr1aPaymentServicesdroptestartifacts&#34;

2020-04-23T09:30:39.2634578Z Starting test execution, please wait...

2020-04-23T09:30:39.4783658Z A total of 1 test files matched the specified pattern.

2020-04-23T09:30:40.8660112Z   X Can\_Get\_MIDs \[521ms\]

2020-04-23T09:30:40.8684249Z   Error Message:

2020-04-23T09:30:40.8684441Z    Test method PaymentServices.IntegrationTests.ControllerMIDTests.Can\_Get\_MIDs threw exception: 

2020-04-23T09:30:40.8684574Z System.BadImageFormatException: Could not load file or assembly &#39;PaymentServices, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null&#39; or one of its dependencies. An attempt was made to load a program with an incorrect format.

2020-04-23T09:30:40.8684766Z   Stack Trace:

2020-04-23T09:30:40.8684881Z       at PaymentServices.IntegrationTests.ControllerMIDTests.Can\_Get\_MIDs()

…

2020-04-23T09:30:40.9038788Z Results File: C:agent\_work\_tempTestResultssvc-devops\_SVRHQAPP027\_2020-04-23\_10\_30\_40.trx

2020-04-23T09:30:40.9080344Z Total tests: 22

2020-04-23T09:30:40.9082348Z      Failed: 22

2020-04-23T09:30:40.9134858Z ##\[error\]Test Run Failed.
</code></pre><h3 id="solution">Solution</h3>
<p>I needed to tell <strong>vstest.console.exe</strong> to run x64 as opposed to it’s default of x32. This can be done with a command line override <strong>–platform:x64</strong></p>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/04/image-1.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/04/image_thumb-1.png" title="image"></a></p>
]]></content:encoded>
    </item>
    <item>
      <title>Swapping my Azure DevOps Pipeline Extensions release process to use Multistage YAML pipelines</title>
      <link>https://blog.richardfennell.net/posts/swapping-my-azure-devops-pipeline-extensions-release-process-to-use-multistage-yaml-pipelines/</link>
      <pubDate>Tue, 07 Apr 2020 10:55:34 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/swapping-my-azure-devops-pipeline-extensions-release-process-to-use-multistage-yaml-pipelines/</guid>
      <description>&lt;p&gt;In the past I have &lt;a href=&#34;https://github.com/rfennell/AzurePipelines/wiki/Outlining-my-Azure-DevOps-CI-CD-Process-using-UI-based-tools&#34;&gt;documented the build and release process I use for my Azure DevOps Pipeline Extensions&lt;/a&gt; and also detailed how I have started &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2019/04/26/migrating-a-gui-based-build-to-yaml-in-azure-devops-pipelines/&#34;&gt;to move the build phases to YAML&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Well now I consider that &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/pipelines/process/stages?view=azure-devops&amp;amp;tabs=yaml&#34;&gt;multistage YAML pipelines&lt;/a&gt; are mature enough to allow me to do my whole release pipeline in YAML, hence this post.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/04/image.png&#34;&gt;&lt;img alt=&#34;image&#34; loading=&#34;lazy&#34; src=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/04/image_thumb.png&#34; title=&#34;image&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My pipeline performs a number of stages, &lt;a href=&#34;https://github.com/rfennell/AzurePipelines/blob/master/Extensions/ArtifactDescription/azure-pipelines.yml&#34;&gt;you can find a sample pipeline here&lt;/a&gt;. Note that I have made every effort to extract variables into variable groups to aid reuse of the pipeline definition. I have added documentation as to where variable are stored and what they are used for.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In the past I have <a href="https://github.com/rfennell/AzurePipelines/wiki/Outlining-my-Azure-DevOps-CI-CD-Process-using-UI-based-tools">documented the build and release process I use for my Azure DevOps Pipeline Extensions</a> and also detailed how I have started <a href="https://blogs.blackmarble.co.uk/rfennell/2019/04/26/migrating-a-gui-based-build-to-yaml-in-azure-devops-pipelines/">to move the build phases to YAML</a>.</p>
<p>Well now I consider that <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/stages?view=azure-devops&amp;tabs=yaml">multistage YAML pipelines</a> are mature enough to allow me to do my whole release pipeline in YAML, hence this post.</p>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/04/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/04/image_thumb.png" title="image"></a></p>
<p>My pipeline performs a number of stages, <a href="https://github.com/rfennell/AzurePipelines/blob/master/Extensions/ArtifactDescription/azure-pipelines.yml">you can find a sample pipeline here</a>. Note that I have made every effort to extract variables into variable groups to aid reuse of the pipeline definition. I have added documentation as to where variable are stored and what they are used for.</p>
<p>The stages are as follows</p>
<h3 id="build">Build</h3>
<p>The build phase does the following</p>
<ul>
<li>
<p>Updates all the TASK.JSON files so that the help text has the correct version number</p>
</li>
<li>
<p>Calls a <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops">YAML template</a> (<a href="https://github.com/rfennell/AzurePipelines/blob/master/YAMLTemplates/build-node-task.yml">build-Node-task</a>) that performs all the tasks to transpile a TypeScript based task – if my extension contained multiple tasks this template would be called a number of time</p>
</li>
<li>
<p>Get NPM packages</p>
</li>
<li>
<p>Run <a href="https://marketplace.visualstudio.com/items?itemName=Snyk.snyk-security-scan">Snyk</a> to check for vulnerabilities – if any vulnerabilities are found the build fails</p>
</li>
<li>
<p>Lint and Transpile the TypeScript – if any issue are found the build fails</p>
</li>
<li>
<p>Run any Unit test and publish results – if any test fail the build fails</p>
</li>
<li>
<p>Package up the task (remove dev dependencies)</p>
</li>
<li>
<p>Download the TFX client</p>
</li>
<li>
<p>Package up the Extension VSIX package and publish as a pipeline artifact.</p>
</li>
</ul>
<h3 id="private">Private</h3>
<p>The private phase does the following</p>
<ul>
<li>
<p>Using another YAML template (<a href="https://github.com/rfennell/AzurePipelines/blob/master/YAMLTemplates/publish-extension.yml">publish-extension</a>) publish the extension to the <a href="https://marketplace.visualstudio.com/search?term=fennell&amp;target=AzureDevOps&amp;category=Azure%20Pipelines&amp;sortBy=Relevance">Azure DevOps Marketplace</a>, but with flags so it is private and only assessible to my account for testing</p>
</li>
<li>
<p>Download the TFX client</p>
</li>
<li>
<p>Publishes the Extension to the Marketplace</p>
</li>
</ul>
<p>This phase is done as a <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/deployment-jobs?view=azure-devops">deployment job</a> and is linked to an environment,. However, there are no special approval requirements are set on this environment. This is because I am happy for the release to be done to the private instance assuming the build phase complete without error.</p>
<h3 id="test">Test</h3>
<p>This is where the pipeline gets interesting. The test phase does the following</p>
<ul>
<li>Runs any integration tests. These could be anything dependant on the extension being deployed. Unfortunately there is no option at present in multistage pipeline for a manual task to say ‘do the manual tests’, but you could simulate similar by sending an email or the like.</li>
</ul>
<p>The clever bit here is that I don’t want this stage to run until the new private version of the extension has been published and is available; there can be a delay between TFX saying the extension is published and it being downloadable by an agent. This can cause a problem in that you think you are running tests against a different version of the extension to one you have. To get around this problem I have implemented a <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&amp;tabs=check-pass">check on the environment</a> this stage’s deployment job is linked to. This check runs an Azure Function to check the version of the extension in the Marketplace. This is <a href="https://blogs.blackmarble.co.uk/rfennell/2018/03/20/using-vsts-gates-to-help-improve-my-deployment-pipeline-of-vsts-extensions-to-the-visual-studio-marketplace/">exactly the same Azure Function I already used in my UI based pipelines to perform the same job</a>.</p>
<p>The only issue here is that this Azure Function is used as an exit gate in my UI based pipelines; to not allow the pipeline to exit the private stage until the extension is publish. I cannot do this in a multistage YAML pipeline as environment checks are only done on entry to the environment. This means I have had to use an extra Test stage to associate the entry check with. This was setup as follows</p>
<ul>
<li>
<p>Create a new environment</p>
</li>
<li>
<p>Click the ellipse (…) and pick ‘approvals and checks’</p>
</li>
<li>
<p>Add a new Azure Function check</p>
</li>
<li>
<p>Provide the details, documented in my <a href="https://blogs.blackmarble.co.uk/rfennell/2018/03/20/using-vsts-gates-to-help-improve-my-deployment-pipeline-of-vsts-extensions-to-the-visual-studio-marketplace/">previous post</a>, to link to your Azure Function. Note that you can, in the ’control options’ section of the configuration, link to a variable group. This is a good place to store all the values, you need to provide</p>
</li>
<li>
<p>URL of the Azure Function</p>
</li>
<li>
<p>Key to us the function</p>
</li>
<li>
<p>The function header</p>
</li>
<li>
<p>The body – this one is interesting. You need to provide the build number and the GUID of a task in the extension for my Azure Function. It would be really good if both of these could be picked up from the pipeline trying to use the environment. This would allow a single ‘test’ environment to be created for use by all my extensions, in the same way there are only a single ‘private’ and ‘public’ environment. However, there is a problem, the build number is picked up OK, but as far as I can see I cannot access custom pipeline variables, so cannot get the task GUID I need dynamically. I assume this is because this environment entry check is run outside of the pipeline. The only solution  can find is to place the task GUID as a hard coded value in the check declaration (or I suppose in the variable group). The downside of this is it means I have to have an environment dedicated to each extension, each with a different task GUID. Not perfect, but not too much of a problem</p>
</li>
<li>
<p>In the Advanced check the check logic</p>
</li>
<li>
<p>In control options link to the variable group contain any variables used.</p>
</li>
</ul>
<h3 id="documentation">Documentation</h3>
<p>The documentation stage again uses a template (<a href="https://github.com/rfennell/AzurePipelines/blob/master/YAMLTemplates/generate-wiki-docs.yml">generate-wiki-docs</a>) and does the following</p>
<ul>
<li>Use the extension and task manifest files to generate YAML usage documentation <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-YAMLGenerator">using one of my tasks</a></li>
<li>Uploads the extension readme file to a WIKI <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-WIKIUpdater-Tasks">using another of my tasks</a></li>
<li>Uploads the extension YAML usage file to a WIKI</li>
</ul>
<h3 id="public">Public</h3>
<p>The public stage is also a deployment job and linked to an environment. This environment has an approval set so I have to approve any release of the public version of the extension.</p>
<p>As well as doing the same as private stage this stage does the following</p>
<ul>
<li>Same a private stage</li>
<li>Send a Tweet saying I have release a new version of the extension using my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-ArtifactDescription-Tasks">Artifact Description</a> extension to get the text of the PR</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-BuildUpdating-Tasks">Update the pipelines minor build number variable</a> to make sure then I next release I have a higher build number</li>
</ul>
<h3 id="summary">Summary</h3>
<p>It took a bit of trial and error to get this going, but I think I have a good solution now. The fact that the bulk of the work is done using shared templates means I should get good reuse of the work I have done. I am sure I will be able to improve the template as time goes on but it is a good start</p>
]]></content:encoded>
    </item>
    <item>
      <title>My Azure DevOps Pipeline is not triggering on a GitHub Pull request - fixed</title>
      <link>https://blog.richardfennell.net/posts/my-azure-devops-pipeline-is-not-triggering-on-a-github-pull-request-fixed/</link>
      <pubDate>Tue, 07 Apr 2020 09:41:06 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/my-azure-devops-pipeline-is-not-triggering-on-a-github-pull-request-fixed/</guid>
      <description>&lt;p&gt;I have recently hit a problem that some of my Azure DevOps YAML pipelines, that I use to build my Azure DevOps Pipeline Extensions, are not triggering on a new PR being created on GitHub.&lt;/p&gt;
&lt;p&gt;I did not get to the bottom of why this is happening, but I found a fix.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check and of make a note of any UI declared variables in your Azure DevOps YAML Pipeline that is not triggering&lt;/li&gt;
&lt;li&gt;Delete the pipeline&lt;/li&gt;
&lt;li&gt;Re-add the pipeline, linking to the YAML file hosted on GitHub. You might be asked to re-authorise the link between Azure DevOps Pipelines and GitHub.&lt;/li&gt;
&lt;li&gt;Re-enter any variables that are declared via the Pipelines UI and save the changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your pipeline should start to be triggered again&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have recently hit a problem that some of my Azure DevOps YAML pipelines, that I use to build my Azure DevOps Pipeline Extensions, are not triggering on a new PR being created on GitHub.</p>
<p>I did not get to the bottom of why this is happening, but I found a fix.</p>
<ul>
<li>Check and of make a note of any UI declared variables in your Azure DevOps YAML Pipeline that is not triggering</li>
<li>Delete the pipeline</li>
<li>Re-add the pipeline, linking to the YAML file hosted on GitHub. You might be asked to re-authorise the link between Azure DevOps Pipelines and GitHub.</li>
<li>Re-enter any variables that are declared via the Pipelines UI and save the changes</li>
</ul>
<p>Your pipeline should start to be triggered again</p>
]]></content:encoded>
    </item>
    <item>
      <title>Experiences setting up Azure Active Directory single sign-on (SSO) integration with GitHub Enterprise</title>
      <link>https://blog.richardfennell.net/posts/experiences-setting-up-azure-active-directory-single-sign-on-sso-integration-with-github-enterprise/</link>
      <pubDate>Mon, 30 Mar 2020 16:29:34 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/experiences-setting-up-azure-active-directory-single-sign-on-sso-integration-with-github-enterprise/</guid>
      <description>&lt;h3 id=&#34;background&#34;&gt;Background&lt;/h3&gt;
&lt;p&gt;GitHub is a great system for individuals and OSS communities for both public and private project. However, corporate customers commonly want more control over their system than the standard GitHub offering. It is for this reason GitHub offers  &lt;a href=&#34;https://github.com/enterprise&#34;&gt;GitHub Enterprise&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For most corporates, the essential feature that GitHub Enterprise offers is the use Single Sign On (SSO) i.e. allowing users to login to GitHub using their corporate directory accounts.&lt;/p&gt;
&lt;p&gt;I wanted to see how easy this was to setup when you are using Azure Active Directory (AAD).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="background">Background</h3>
<p>GitHub is a great system for individuals and OSS communities for both public and private project. However, corporate customers commonly want more control over their system than the standard GitHub offering. It is for this reason GitHub offers  <a href="https://github.com/enterprise">GitHub Enterprise</a>.</p>
<p>For most corporates, the essential feature that GitHub Enterprise offers is the use Single Sign On (SSO) i.e. allowing users to login to GitHub using their corporate directory accounts.</p>
<p>I wanted to see how easy this was to setup when you are using Azure Active Directory (AAD).</p>
<p>Luckily there is a <a href="https://docs.microsoft.com/en-us/azure/active-directory/saas-apps/github-tutorial">step by step tutorial from Microsoft</a> on how to set this up. Though, I would say that though detailed this tutorial has a strange structure in that it shows the default values not the correct values. Hence, the tutorial requires close reading, don’t just look at the pictures!</p>
<p>Even with close reading, I still hit a problem, all of my own making, as I went through this tutorial.</p>
<h3 id="the-issue--a-stray--in-a-url">The Issue – a stray / in a URL</h3>
<p>I entered all the AAD URLs and certs as instructed (or so I thought) by the tutorial into the Security page of GitHub Enterprise.</p>
<p>When I pressed the ‘Validate’ button in GitHub, to test the SSO settings, I got an error</p>
<p><em>‘The client has not listed any permissions for &lsquo;AAD Graph&rsquo; in the requested permissions in the client&rsquo;s application registration’</em></p>
<p>This sent me shown a rabbit hole looking at user permissions. That wasted a lot of time.</p>
<p>However, it turns out the issue was that I had a // in a URL when it should have been a  /. This was because I had made a cut and paste error when editing the tutorial’s sample URL and adding my organisation details.</p>
<p>Once I fixed this typo the validation worked, I was able to complete the setup and then I could to invite my AAD users to my GitHub Enterprise organisation.</p>
<h3 id="summary">Summary</h3>
<p>So the summary is, if you follow the tutorial setting up SSO from AAD to GitHub Enterprise is easy enough to do, just be careful of over the detail.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Where did all my test results go?</title>
      <link>https://blog.richardfennell.net/posts/where-did-all-my-test-results-go/</link>
      <pubDate>Thu, 05 Mar 2020 13:57:27 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/where-did-all-my-test-results-go/</guid>
      <description>&lt;h3 id=&#34;problem&#34;&gt;Problem&lt;/h3&gt;
&lt;p&gt;I recently tripped myself up whist adding SonarQube analysis to a rather complex Azure DevOps build.&lt;/p&gt;
&lt;p&gt;The build has two VsTest steps, both were using the same folder for their test result files. When the first VsTest task ran it created the expected .TRX and .COVERAGE files and then published its results to Azure DevOps, but when the second VsTest task ran it over wrote this folder, deleting the files already present, before it generated and published it results.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="problem">Problem</h3>
<p>I recently tripped myself up whist adding SonarQube analysis to a rather complex Azure DevOps build.</p>
<p>The build has two VsTest steps, both were using the same folder for their test result files. When the first VsTest task ran it created the expected .TRX and .COVERAGE files and then published its results to Azure DevOps, but when the second VsTest task ran it over wrote this folder, deleting the files already present, before it generated and published it results.</p>
<p>This meant that the build itself had all the test results published, but when SonarQube looked for the files for analysis only the second set of test were present, so its analysis was incorrect.</p>
<h3 id="solution">Solution</h3>
<p>The solution was easy, use different folders for each set of test results.</p>
<p>This gave me a build, the key items are shown below, where one VsTest step does not overwrite the previous results before they can be processed by any 3rd party tasks such as SonarQube.</p>
<pre tabindex="0"><code>steps:  
- task: SonarSource.sonarqube.15B84CA1-B62F-4A2A-A403-89B77A063157.SonarQubePrepare@4  
   displayName: &#39;Prepare analysis on SonarQube&#39;  
   inputs:  
     SonarQube: Sonarqube  
     projectKey: &#39;Services&#39;  
     projectName: &#39;Services&#39;  
     projectVersion: &#39;$(major).$(minor)&#39;  
     extraProperties: |  
      # Additional properties that will be passed to the scanner,   
      sonar.cs.vscoveragexml.reportsPaths=$(System.DefaultWorkingDirectory)/\*\*/\*.coveragexml  
      sonar.cs.vstest.reportsPaths=$(System.DefaultWorkingDirectory)/\*\*/\*.trx

  

… other build steps

  

\- task: VSTest@2  
   displayName: &#39;VsTest – Internal Services&#39;  
   inputs:  
     testAssemblyVer2: |  
      \*\*\*.unittests.dll  
      !\*\*obj\*\*  
     searchFolder: &#39;$(System.DefaultWorkingDirectory)/src/Services&#39;  
     resultsFolder: &#39;$(System.DefaultWorkingDirectory)TestResultsServices&#39;  
     overrideTestrunParameters: &#39;-DeploymentEnabled false&#39;  
     codeCoverageEnabled: true  
     testRunTitle: &#39;Services Unit Tests&#39;  
     diagnosticsEnabled: True  
   continueOnError: true

\- task: VSTest@2  
   displayName: &#39;VsTest - External&#39;  
   inputs:  
     testAssemblyVer2: |  
      \*\*\*.unittests.dll  
      !\*\*obj\*\*  
     searchFolder: &#39;$(System.DefaultWorkingDirectory)/src/ExternalServices&#39;  
     resultsFolder: &#39;$(System.DefaultWorkingDirectory)TestResultsExternalServices&#39;  
     vsTestVersion: 15.0  
     codeCoverageEnabled: true  
     testRunTitle: &#39;External Services Unit Tests&#39;  
     diagnosticsEnabled: True  
   continueOnError: true  

\- task: BlackMarble.CodeCoverage-Format-Convertor-Private.CodeCoverageFormatConvertor.CodeCoverage-Format-Convertor@1  
   displayName: &#39;CodeCoverage Format Convertor&#39;  
   inputs:  
     ProjectDirectory: &#39;$(System.DefaultWorkingDirectory)&#39;  

\- task: SonarSource.sonarqube.6D01813A-9589-4B15-8491-8164AEB38055.SonarQubeAnalyze@4  
   displayName: &#39;Run Code Analysis&#39;

\- task: SonarSource.sonarqube.291ed61f-1ee4-45d3-b1b0-bf822d9095ef.SonarQubePublish@4  
   displayName: &#39;Publish Quality Gate Result&#39;  
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>You need to pass a GitHub PAT to create Azure DevOps Agent Images using Packer</title>
      <link>https://blog.richardfennell.net/posts/you-need-to-pass-a-github-pat-to-create-azure-devops-agent-images-using-packer/</link>
      <pubDate>Mon, 02 Mar 2020 11:52:03 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/you-need-to-pass-a-github-pat-to-create-azure-devops-agent-images-using-packer/</guid>
      <description>&lt;p&gt;I wrote recently about &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2019/12/21/creating-hyper-v-hosted-azure-devops-private-agents-based-on-the-same-vm-images-as-used-by-microsoft-for-their-hosted-agents/&#34;&gt;Creating Hyper-V hosted Azure DevOps Private Agents based on the same VM images as used by Microsoft for their Hosted Agent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As discussed in that post, using this model you will recreate your build agent VMs on a regular basis, as opposed to patching them. When I came to do this recently I found that the Packer image generation was failing with errors related to accessing packages.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I wrote recently about <a href="https://blogs.blackmarble.co.uk/rfennell/2019/12/21/creating-hyper-v-hosted-azure-devops-private-agents-based-on-the-same-vm-images-as-used-by-microsoft-for-their-hosted-agents/">Creating Hyper-V hosted Azure DevOps Private Agents based on the same VM images as used by Microsoft for their Hosted Agent</a>.</p>
<p>As discussed in that post, using this model you will recreate your build agent VMs on a regular basis, as opposed to patching them. When I came to do this recently I found that the Packer image generation was failing with errors related to accessing packages.</p>
<p>Initially, I did not read the error message too closely and just assumed it was an intermittent issue as I had found you sometime get random timeouts with this process. However, when the problem did not go away after repeated retries I realised I had a more fundamental problem, so read the log properly!</p>
<p>Turns out the issue is you now have to pass a GitHub PAT token that has at least read access to the packages feed to allow Packer to authenticate with GitHub to read packages.</p>
<p>The process to create the required PAT is as follows</p>
<ol>
<li>In a browser login to GitHub</li>
<li>Click your profile (top right)</li>
<li>Select Settings</li>
<li>Pick Developer Settings</li>
<li>Pick Personal Access Tokens and create a new one that has <strong>read:packages</strong> enabled</li>
</ol>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/03/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2020/03/image_thumb.png" title="image"></a></p>
<p>Once created, this PAT needs to be passed into Packer. If using the settings JSON file this is just another variable</p>
<pre tabindex="0"><code>{
&#34;client\_id&#34;: &#34;Azure Client ID&#34;,
&#34;client\_secret&#34;: &#34;Client Secret&#34;,
&#34;tenant\_id&#34;: &#34;Azure Tenant ID&#34;,
&#34;subscription\_id&#34;: &#34;Azure Sub ID&#34;,
&#34;object\_id&#34;: &#34;The object ID for the AAD SP&#34;,
&#34;location&#34;: &#34;Azure location to use&#34;,
&#34;resource\_group&#34;: &#34;Name of resource group that contains Storage Account&#34;,
&#34;storage\_account&#34;: &#34;Name of the storage account&#34;,
&#34;ssh\_password&#34;: A password&#34;,
&#34;install\_password&#34;: &#34;A password&#34;,
&#34;commit\_url&#34;: &#34;A url to to be save in a text file on the VHD, usually the URL if commit VHD based on&#34;,

&#34;github\_feed\_token&#34;: &#34;A PAT&#34;

}  
</code></pre><p>If you are running Packer within a build pipeline, as the other blog post discusses, then the PAT will be another build variable.</p>
<p>Once this change was made I was able to get Packer to run to completion, as expected.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cannot queue a new build on Azure DevOps Server 2019.1 due to the way a SQL cluster was setup</title>
      <link>https://blog.richardfennell.net/posts/cannot-queue-a-new-build-on-azure-devops-server-2019-1-due-to-the-way-a-sql-cluster-was-setup/</link>
      <pubDate>Thu, 17 Oct 2019 19:24:13 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/cannot-queue-a-new-build-on-azure-devops-server-2019-1-due-to-the-way-a-sql-cluster-was-setup/</guid>
      <description>&lt;p&gt;I have recently been doing a TFS 2015 to Azure DevOps Server 2019.1 upgrade for a client. The first for a while, I have been working with Azure DevOps Service mostly of late. Anyway I saw an issue I had never seen before with any version of TFS, and I could find no information on the Internet.&lt;/p&gt;
&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;The error occurred when I tried to queue a new build after the upgrade, the build instantly failed with the error&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have recently been doing a TFS 2015 to Azure DevOps Server 2019.1 upgrade for a client. The first for a while, I have been working with Azure DevOps Service mostly of late. Anyway I saw an issue I had never seen before with any version of TFS, and I could find no information on the Internet.</p>
<h3 id="the-problem">The Problem</h3>
<p>The error occurred when I tried to queue a new build after the upgrade, the build instantly failed with the error</p>
<p><em>‘The module being executed is not trusted, Either the owner of the database of the module need to be granted authenticate permission, or the module needs to be digitally signed. Warning: Null value is eliminated by an aggregate or other SET operation, The statement has been terminated’.</em></p>
<h3 id="the-solution">The Solution</h3>
<p>It turns out the issue was the the client was using a enterprise wide SQL cluster to host the <em>tfs_</em> databases. After the Azure DevOps upgrade the DBAs has enabled a trigger based logging system to monitor the databases and this was causing the error.</p>
<p>As soon as this logging was switched off everything worked as expected.</p>
<p>I would not recommend using such a logging tool for any ‘out the box’ database for a product such as TFS/Azure DevOps Server where the DBA team don’t own the database schema’s changes, as these databases will only occur if the product is upgraded</p>
]]></content:encoded>
    </item>
    <item>
      <title>Strange issue with multiple calls to the same REST WebClient in PowerShell</title>
      <link>https://blog.richardfennell.net/posts/strange-issue-with-multiple-calls-to-the-same-rest-webclient-in-powershell/</link>
      <pubDate>Thu, 29 Aug 2019 11:56:05 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/strange-issue-with-multiple-calls-to-the-same-rest-webclient-in-powershell/</guid>
      <description>&lt;p&gt;Hit a strange problem today trying to do a simple Work Item update via the Azure DevOps REST API.&lt;/p&gt;
&lt;p&gt;To do a &lt;a href=&#34;https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/work%20items/update?view=azure-devops-rest-5.1&#34;&gt;WI update you need to call the REST API&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using the verb PATCH&lt;/li&gt;
&lt;li&gt;With the Header “Content-Type” set to “application/json-patch+json”&lt;/li&gt;
&lt;li&gt;Include in the Body the current WI update revision (to make sure you are updating the current version)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the first step is to get the current WI values to find the current revision.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hit a strange problem today trying to do a simple Work Item update via the Azure DevOps REST API.</p>
<p>To do a <a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/wit/work%20items/update?view=azure-devops-rest-5.1">WI update you need to call the REST API</a></p>
<ul>
<li>Using the verb PATCH</li>
<li>With the Header “Content-Type” set to “application/json-patch+json”</li>
<li>Include in the Body the current WI update revision (to make sure you are updating the current version)</li>
</ul>
<p>So the first step is to get the current WI values to find the current revision.</p>
<p>So my update logic was along the lines of</p>
<ol>
<li>Create new WebClient with the Header “Content-Type” set to “application/json-patch+json”</li>
<li>Do a Get call to API to get the current work item</li>
<li>Build the update payload with my updated fields and the current revision.</li>
<li>Do a PATCH call to API using the client created in step 1 to update the current work item</li>
</ol>
<p>Problem was at Step 4 I got a 400 error. A general error, not too helpful</p>
<p>After much debugging I spotted the issue was that after Step 2. my WebClient’s Headers had changed, I had lost the content type – no idea why.</p>
<p>It all started to work if I recreated my WebClient after Step 2, so something like (in PowrShell)</p>
<pre tabindex="0"><code>Function Get-WebClient

{

param  
(

&gt; \[string\]$pat,  
&gt; \[string\]$ContentType = &#34;application/json&#34;

)

&gt; $wc = New-Object System.Net.WebClient  
&gt; $pair = &#34;:${password}&#34;  
&gt; $bytes = \[System.Text.Encoding\]::ASCII.GetBytes($pair)  
&gt; $base64 = \[System.Convert\]::ToBase64String($bytes)  
&gt; $wc.Headers.Add(“Authorization”,&#34;Basic $base64&#34;)  
&gt; $wc.Headers\[&#34;Content-Type&#34;\] = $ContentType  
&gt; $wc

}

  
</code></pre><p>function Update-WorkItemTitle {</p>
<p>param</p>
<p>(</p>
<blockquote>
<p>$baseUri ,<br>
$teamproject,<br>
$workItemID,<br>
$pat,<br>
$title</p></blockquote>
<p>)</p>
<blockquote>
<p>$wc = Get-WebClient -pat $pat -ContentType &ldquo;application/json-patch+json&rdquo;<br>
$uri = &ldquo;$($baseUri)/$teamproject/_apis/wit/workitems/$($workItemID)?api-version=5.1&rdquo;</p></blockquote>
<blockquote>
<p># you can only update a work item if you also pass in the rev, this makes sure you are updating lastest version<br>
$jsondata = $wc.DownloadString($uri) | ConvertFrom-Json</p></blockquote>
<blockquote>
<p>$wc = Get-WebClient -pat $pat -ContentType &ldquo;application/json-patch+json&rdquo;</p></blockquote>
<blockquote>
<p>$data = @(<br>
@{<br>
            op    = &ldquo;test&rdquo;;<br>
            path  = &ldquo;/rev&rdquo;;<br>
            value = $jsondata.Rev<br>
},<br>
@{<br>
            op    = &ldquo;add&rdquo;;<br>
            path  = &ldquo;/fields/System.Title&rdquo;;<br>
            value = $title<br>
}<br>
) | ConvertTo-Json</p></blockquote>
<blockquote>
<p>$jsondata = $wc.UploadString($uri, &ldquo;PATCH&rdquo;, $data) | ConvertFrom-Json<br>
$jsondata</p></blockquote>
<p>}</p>
<pre tabindex="0"><code></code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Authentication loops swapping organisations in Azure DevOps</title>
      <link>https://blog.richardfennell.net/posts/authentication-loops-swapping-organisations-in-azure-devops/</link>
      <pubDate>Tue, 13 Aug 2019 11:04:36 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/authentication-loops-swapping-organisations-in-azure-devops/</guid>
      <description>&lt;p&gt;I have recently been getting a problem swapping between different organisations in Azure DevOps. It happens when I swap between Black Mable ones and customer ones, where each is back by different Azure Active Directory (AAD) but I am using the same credentials; because I am either a member of that AAD or a guest.&lt;/p&gt;
&lt;p&gt;The problem is I get into an authentication loop. It happens to be in Chrome, but you might find the same problem in other browsers.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have recently been getting a problem swapping between different organisations in Azure DevOps. It happens when I swap between Black Mable ones and customer ones, where each is back by different Azure Active Directory (AAD) but I am using the same credentials; because I am either a member of that AAD or a guest.</p>
<p>The problem is I get into an authentication loop. It happens to be in Chrome, but you might find the same problem in other browsers.</p>
<p>It seems to be a recent issue, maybe related to MFA changes in AAD?</p>
<p>I used to be re-promoted for my ID when I swapped organisations in a browser tab, but not asked for further authentication</p>
<p>However, now the following happens</p>
<ul>
<li>I login to an organisation without a problem e.g <a href="https://dev.azure.com/someorg">https://dev.azure.com/someorg</a> using ID, password and MFA</li>
<li>In the same browser window, when I connect to another organisation e.g. <a href="https://dev.azure.com/someotherorg">https://dev.azure.com/someotherorg</a> </li>
<li>I am asked to pick an account, then there is the MFA challenge, but then go back to the login</li>
<li>…. and repeat.</li>
</ul>
<p>The fix is to go in the browser tab to <a href="https://dev.azure.com">https://dev.azure.com</a>. As you are already authenticated you will be able to sign out, then all is OK, you can login again.</p>
<p>The other options is to make even more use of <a href="https://support.google.com/chrome/answer/2364824?co=GENIE.Platform%3DDesktop&amp;hl=en">Chrome People</a>; one ‘person’ per customer, as opposed to my current usage on one ‘person’ per ID</p>
]]></content:encoded>
    </item>
    <item>
      <title>Azure DevOps Repos branch build policies not triggering when expected in PRs  - Solved</title>
      <link>https://blog.richardfennell.net/posts/azure-devops-repos-branch-build-policies-not-triggering-when-expected-in-prs-solved/</link>
      <pubDate>Thu, 23 May 2019 15:01:36 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/azure-devops-repos-branch-build-policies-not-triggering-when-expected-in-prs-solved/</guid>
      <description>&lt;p&gt;I recently hit a problem with builds triggered by &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&#34;&gt;branch policies in Azure DevOps Repos&lt;/a&gt;. With the help of Microsoft I found out the problem and I thought it worth writing up uncase others hit the issue.&lt;/p&gt;
&lt;h3 id=&#34;setup&#34;&gt;Setup&lt;/h3&gt;
&lt;h4 id=&#34;folders&#34;&gt;Folders&lt;/h4&gt;
&lt;p&gt;Assume you have a Git repo with source for the UI, backend Services and common code in sub folders&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;/ [root]&lt;br&gt;
     UI&lt;br&gt;
     Services&lt;br&gt;
     Common&lt;/p&gt;&lt;/blockquote&gt;
&lt;h4 id=&#34;branch-policies&#34;&gt;Branch Policies&lt;/h4&gt;
&lt;p&gt;On the Master branch there are a policies of running&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I recently hit a problem with builds triggered by <a href="https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops">branch policies in Azure DevOps Repos</a>. With the help of Microsoft I found out the problem and I thought it worth writing up uncase others hit the issue.</p>
<h3 id="setup">Setup</h3>
<h4 id="folders">Folders</h4>
<p>Assume you have a Git repo with source for the UI, backend Services and common code in sub folders</p>
<blockquote>
<p>/ [root]<br>
     UI<br>
     Services<br>
     Common</p></blockquote>
<h4 id="branch-policies">Branch Policies</h4>
<p>On the Master branch there are a policies of running</p>
<ul>
<li>one build for anything in the UI folder/project or common folder/project</li>
<li>and a different build for anything in the Services folder/project or common folder/project</li>
</ul>
<p>These build were filtered by path using the filters</p>
<blockquote>
<p>/UX; /Common<br>
/Services; /Common</p></blockquote>
<h3 id="the-issue">The Issue</h3>
<p>I discovered the problem by doing the following</p>
<ul>
<li>Create a PR for some work that effects the UI project</li>
<li>As expected the UI build triggers</li>
<li>Update the PR with a second commit for the Services code</li>
<li>The Service build is <strong>not</strong> triggered</li>
</ul>
<h3 id="the-solution">The Solution</h3>
<p>The fix was simple it turns out. Remove the spaces from the filter paths so they become</p>
<blockquote>
<p>/UX;/Common<br>
/Services;/Common</p></blockquote>
<p>Once this was done the builds triggered as expected.</p>
<p>Thanks again to the Azure DevOps Product Group for the help</p>
]]></content:encoded>
    </item>
    <item>
      <title>Regex issues in Node</title>
      <link>https://blog.richardfennell.net/posts/regex-issues-in-node/</link>
      <pubDate>Thu, 23 May 2019 13:13:17 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/regex-issues-in-node/</guid>
      <description>&lt;p&gt;I have been trying to use Regex to select a block of an XML based .NET Core CSPROJ file, and yes before you say know I could use XPATH, but why am not is another story.&lt;/p&gt;
&lt;p&gt;I was trying to use the Regex&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;content.match(/&amp;lt;PropertyGroup&amp;gt;((.|n)\*)&amp;lt;/PropertyGroup&amp;gt;/gmi)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The strange thing was this selection string worked in online Regex testers and in online Javascript IDEs, but failed inside my Node based Azure DevOps Pipeline extension.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have been trying to use Regex to select a block of an XML based .NET Core CSPROJ file, and yes before you say know I could use XPATH, but why am not is another story.</p>
<p>I was trying to use the Regex</p>
<pre tabindex="0"><code>content.match(/&lt;PropertyGroup&gt;((.|n)\*)&lt;/PropertyGroup&gt;/gmi)
</code></pre><p>The strange thing was this selection string worked in online Regex testers and in online Javascript IDEs, but failed inside my Node based Azure DevOps Pipeline extension.</p>
<p>After much experimentation I found that the following line worked</p>
<pre tabindex="0"><code>content.match(/&lt;PropertyGroup&gt;(\[sS\]\*?)&lt;/PropertyGroup&gt;/gmi)

  
</code></pre><p>Well that a a good few hours of my life I won’t get back. No idea why Node handles the wildcards differently</p>
]]></content:encoded>
    </item>
    <item>
      <title>A fix for Error: SignerSign() failed.&amp;quot; (-2146958839/0x80080209) with SignTool.exe</title>
      <link>https://blog.richardfennell.net/posts/a-fix-for-error-signersign-failed-2146958839-0x80080209-with-signtool-exe/</link>
      <pubDate>Tue, 30 Apr 2019 15:56:21 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/a-fix-for-error-signersign-failed-2146958839-0x80080209-with-signtool-exe/</guid>
      <description>&lt;p&gt;I have spent too long recently trying to sign a UWP .MSIXBUNDLE generated from an Azure DevOps build using the SignTool.exe and our code signing certificate. I kept getting the error&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Done Adding Additional Store  
Error information: &amp;#34;Error: SignerSign() failed.&amp;#34; (-2146958839/0x80080209)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From past experience, SignTool errors are usually due to the publisher details in the XML manifest files (in this case unpack the bundle with MakeAppx.exe and look in AppxMetadataAppxBundleManifest.xml, and also check the manifest in the bundled .MSIX files) does not match the subject details for the PFX file being used for signing. &lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have spent too long recently trying to sign a UWP .MSIXBUNDLE generated from an Azure DevOps build using the SignTool.exe and our code signing certificate. I kept getting the error</p>
<pre tabindex="0"><code>Done Adding Additional Store  
Error information: &#34;Error: SignerSign() failed.&#34; (-2146958839/0x80080209)
</code></pre><p>From past experience, SignTool errors are usually due to the publisher details in the XML manifest files (in this case unpack the bundle with MakeAppx.exe and look in AppxMetadataAppxBundleManifest.xml, and also check the manifest in the bundled .MSIX files) does not match the subject details for the PFX file being used for signing. </p>
<p>Or so I thought…..</p>
<p>Turns out you can get this error too if you use the wrong version of the SignTool, but it give no clue to this fact.</p>
<p>So the top tip is …</p>
<p>Make sure you use the SignTool.exe from the same folder as the MakeAppx.exe tool. In  my case in “C:Program Files (x86)Windows Kits10bin10.0.17763.0x64”</p>
<p>Once I did this, after of course updating all the manifest files with the correct publisher details, I was able to sign my bundle as I wanted.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Programmatically adding User Capabilities to Azure DevOps Agents</title>
      <link>https://blog.richardfennell.net/posts/programmatically-adding-user-capabilities-to-azure-devops-agents/</link>
      <pubDate>Thu, 06 Dec 2018 14:56:58 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/programmatically-adding-user-capabilities-to-azure-devops-agents/</guid>
      <description>&lt;p&gt;I am automating the process by which we keep our build agent up to date. The basic process is to use a fork of the standard Microsoft Azure DevOps Pipeline agent that has the additional code included we need, &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2018/02/27/building-private-vsts-build-agents-using-the-microsoft-packer-based-agent-image-creation-model/&#34;&gt;notably Biztalk&lt;/a&gt;. Once I have the Packer created VM up and running, I need to install the agent. This is well document, just run _.config.cmd –help _for details. However, there is no option to add user capabilities to the agent. I know I could set them via environment variables, but I don’t want the same user capabilities on each agent on a VM (we use multiple agents on a single VM). There was no documented Azure DevOps API I could find to add capabilities, but a bit of hacking around with Chrome Dev tools and Postman got me a solution, which I have provided a&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I am automating the process by which we keep our build agent up to date. The basic process is to use a fork of the standard Microsoft Azure DevOps Pipeline agent that has the additional code included we need, <a href="https://blogs.blackmarble.co.uk/rfennell/2018/02/27/building-private-vsts-build-agents-using-the-microsoft-packer-based-agent-image-creation-model/">notably Biztalk</a>. Once I have the Packer created VM up and running, I need to install the agent. This is well document, just run _.config.cmd –help _for details. However, there is no option to add user capabilities to the agent. I know I could set them via environment variables, but I don’t want the same user capabilities on each agent on a VM (we use multiple agents on a single VM). There was no documented Azure DevOps API I could find to add capabilities, but a bit of hacking around with Chrome Dev tools and Postman got me a solution, which I have provided a</p>
<script src="https://gist.github.com/rfennell/13b014fc816822cc9007ae26cc2cb43f.js"></script>
]]></content:encoded>
    </item>
    <item>
      <title>Keeping Azure DevOps organisations inherited process templates in sync</title>
      <link>https://blog.richardfennell.net/posts/keeping-azure-devops-organisations-inherited-process-templates-in-sync/</link>
      <pubDate>Thu, 29 Nov 2018 17:27:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/keeping-azure-devops-organisations-inherited-process-templates-in-sync/</guid>
      <description>&lt;h3 id=&#34;the-problem&#34;&gt;The problem&lt;/h3&gt;
&lt;p&gt;If you are like me for historic reasons you have multiple Azure DevOps organisations (instances) backed by the same Azure Active Directory (AAD). In my case for example: one was created when Azure DevOps was first released as &lt;em&gt;TFSPreview.com&lt;/em&gt; and another is from our migration from on-prem TFS using the &lt;a href=&#34;https://azure.microsoft.com/en-us/services/devops/migrate/&#34;&gt;DB Migration Tools&lt;/a&gt; method; and I have others. I make active use of all of these for different purposes, though one is primary with the majority of work done on it, and so I want to make sure the &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/organizations/settings/work/manage-process?view=vsts&amp;amp;tabs=new-nav&#34;&gt;inherited process templates&lt;/a&gt; are the same on each of them. Using the primary organisation as the master customisation. &lt;strong&gt;Note&lt;/strong&gt; I have already &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/devops/reference/on-premises-xml-process-model?view=vsts&#34;&gt;converted all my old on-premises XML process models&lt;/a&gt; to inherited process templates. There is no out the box way to do keep processes in syncs, but it is possible using a few tools. The main one is the &lt;a href=&#34;https://github.com/Microsoft/process-migrator&#34;&gt;Microsoft Process Migrator for Node&lt;/a&gt; on GitHub.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="the-problem">The problem</h3>
<p>If you are like me for historic reasons you have multiple Azure DevOps organisations (instances) backed by the same Azure Active Directory (AAD). In my case for example: one was created when Azure DevOps was first released as <em>TFSPreview.com</em> and another is from our migration from on-prem TFS using the <a href="https://azure.microsoft.com/en-us/services/devops/migrate/">DB Migration Tools</a> method; and I have others. I make active use of all of these for different purposes, though one is primary with the majority of work done on it, and so I want to make sure the <a href="https://docs.microsoft.com/en-us/azure/devops/organizations/settings/work/manage-process?view=vsts&amp;tabs=new-nav">inherited process templates</a> are the same on each of them. Using the primary organisation as the master customisation. <strong>Note</strong> I have already <a href="https://docs.microsoft.com/en-us/azure/devops/reference/on-premises-xml-process-model?view=vsts">converted all my old on-premises XML process models</a> to inherited process templates. There is no out the box way to do keep processes in syncs, but it is possible using a few tools. The main one is the <a href="https://github.com/Microsoft/process-migrator">Microsoft Process Migrator for Node</a> on GitHub.</p>
<h3 id="the-solution">The Solution</h3>
<p>Firstly I cloned the Microsoft Process Migrator and built it as per the instructions on the repo. I created a config file and then ran the tool. On one organisation it ran fine. However on another I had errors like: [ERROR] [2018-11-26T14:35:44.880Z] Process import validation failed. Process with same name already exists on target account. <em>[ERROR] [2018-11-26T14:39:54.206Z] Import failed, see log file for details. Create field &lsquo;Location&rsquo; failed, see log for details</em> This was because I had in the past manually duplicated the inherited process template onto this organisation, so there was a process with the same name and fields of the same names. The first error was easy to fix, import the template with a new (temporary) name. The second is more problematic. I had two choice</p>
<ul>
<li>A manual fix</li>
<li>An automated fix using <a href="https://marketplace.visualstudio.com/items?itemName=nkdagility.vsts-sync-migration">Migration Tools for Azure DevOps</a> from <a href="https://marketplace.visualstudio.com/publishers/nkdagility">Martin Hinshelwood</a></li>
</ul>
<p>As I only had a few duplicated unused fields on a single organisation I picked the former. If I had many organisations to sort out I would picked the latter. So my process ended up being</p>
<ol>
<li>Run the Microsoft Process Migrator to migrate ‘My Process’ on the source organisation to ‘My Process 1’ on the target organisation</li>
<li>It gave an error, providing the name of the duplicated field</li>
<li>I checked on the target organisation using a work item query that the field was empty or only had defaulted data (if it had not been I would have used Martin’s tool to migrate the data to a temporary field and then deleted the problem field, moving the data back to the correct field from the temporary field when the import of the process template was completed)</li>
<li>I deleted the field from the work item type that referenced it</li>
<li>I deleted the field</li>
<li>I deleted the process template ‘My Process 1’, a failed import leaves a half created process</li>
<li>I went back to step 1 and repeated until the import completed without error</li>
<li>I tested my migrated inherited process was OK</li>
<li>On the target organisation I then renamed ‘My Process’ to ‘My Process – Old’</li>
<li>I then renamed ‘My Process 1’ to ‘My Process’</li>
<li>In my case I also made ‘My Process’ as the default, you might not do this if another process is the default, but step 13 does require the process template is not the default</li>
<li>I moved all the team projects using the process template now called ‘My Process – Old’ to ‘My Process’</li>
<li>I was then able to delete the process template ‘My Process – Old’ as it has no associated team projects and was not the default</li>
</ol>
<p>As I customise my primary organisation’s process templates I can repeat this process to keep the processes in sync between organisations.  Note that in future migrations I won’t have to do steps 2..6 as there are no manually created duplicated fields. So it should be more straight forward. So a valid solution until any similar functionality is built into Azure DevOps, and there is no sign of that on the roadmap.</p>
]]></content:encoded>
    </item>
    <item>
      <title>DPI problems after upgrading from Camtasia 8 to 2018</title>
      <link>https://blog.richardfennell.net/posts/dpi-problems-after-upgrading-from-camtasia-8-to-2018/</link>
      <pubDate>Mon, 26 Nov 2018 14:18:06 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/dpi-problems-after-upgrading-from-camtasia-8-to-2018/</guid>
      <description>&lt;p&gt;&lt;em&gt;This is another of those posts I do so I don’t forget how I fixed something.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I have a requirement to record videos for a client in 720p resolution. As I use as SurfaceBook with a High-Res screen I have found the best way to do this is set my Windows screen resolution to 1280x720 and do all my recording at this as native resolution. Any attempt to record smaller portions of a screen or scale video in production have lead to quality problems, especially as remote desktops within remote desktops are required.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>This is another of those posts I do so I don’t forget how I fixed something.</em></p>
<p>I have a requirement to record videos for a client in 720p resolution. As I use as SurfaceBook with a High-Res screen I have found the best way to do this is set my Windows screen resolution to 1280x720 and do all my recording at this as native resolution. Any attempt to record smaller portions of a screen or scale video in production have lead to quality problems, especially as remote desktops within remote desktops are required.</p>
<p>This has been working fine with Camtasia 8, but when I upgrade to Camtasia  2018.0.7 I got problems. The whole UI of the tool was unusable, it ignored the resizing/DPI changes.</p>
<p>The only fix I could find was to create a desktop shortcut to the EXE and set the Properties &gt; Compatibility &gt; Change high DPI settings &gt; and check the ‘Override high DPI scaling behaviour’ and set this to ‘System’.</p>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/11/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/11/image_thumb.png" title="image"></a></p>
<p>Even after doing this I still found the preview in the editing screen a little blurred, but usable. The final produced MP4s were OK.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using Paths in PR Triggers on an Azure DevOps Pipelines Builds</title>
      <link>https://blog.richardfennell.net/posts/using-paths-in-pr-triggers-on-an-azure-devops-pipelines-builds/</link>
      <pubDate>Tue, 02 Oct 2018 19:49:08 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-paths-in-pr-triggers-on-an-azure-devops-pipelines-builds/</guid>
      <description>&lt;p&gt;When I started creating OSS extensions for Azure DevOps Pipelines (starting on TFSPreview, then VSO, then VSTS and now named Azure DevOps) I made the mistake of putting all my extensions in a single GitHub repo. I thought this would make life easier, I was wrong, it should have been a repo per extension.&lt;/p&gt;
&lt;p&gt;I have considered splitting the GitHub repo, but as a number of people have forked it, over 100 at the last count, I did not want to start a chain of chaos for loads of people.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>When I started creating OSS extensions for Azure DevOps Pipelines (starting on TFSPreview, then VSO, then VSTS and now named Azure DevOps) I made the mistake of putting all my extensions in a single GitHub repo. I thought this would make life easier, I was wrong, it should have been a repo per extension.</p>
<p>I have considered splitting the GitHub repo, but as a number of people have forked it, over 100 at the last count, I did not want to start a chain of chaos for loads of people.</p>
<p>This initial choice has meant that until very recently I could not use the Pull Request triggers in Azure DevOps Pipelines against my GitHub repo. This was because all builds associated with the repo triggered on any extension PR. So, I had to trigger builds manually, providing the branch name by hand. A bit of a pain, and prone to error.</p>
<p>I am pleased to say that with the roll out of <a href="https://docs.microsoft.com/en-us/azure/devops/release-notes/2018/sprint-140-update">Sprint 140</a> we now get the option to add a path filter to PR triggers on builds linked to GitHub repo; something we have had for Azure DevOps hosted Git repos since <a href="https://docs.microsoft.com/en-us/azure/devops/release-notes/2017/nov-28-vsts">Sprint 126</a>.</p>
<p>So now my <a href="https://blogs.blackmarble.co.uk/rfennell/2018/03/20/using-vsts-gates-to-help-improve-my-deployment-pipeline-of-vsts-extensions-to-the-visual-studio-marketplace/">release process</a> is improved. If I add a path filter as shown below, my build and hence release process trigger on a PR just as I need.</p>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/10/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/10/image_thumb.png" title="image"></a></p>
<p>It is just a shame that the GitHub PR only checks the build, not the whole release, before saying all is OK. Hope we see linking to complete Azure DevOps Pipelines in the future.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Videos do not play in VSTS WIKI via relative links - workaround</title>
      <link>https://blog.richardfennell.net/posts/videos-do-not-play-in-vsts-wiki-via-relative-links-workaround/</link>
      <pubDate>Fri, 31 Aug 2018 12:01:54 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/videos-do-not-play-in-vsts-wiki-via-relative-links-workaround/</guid>
      <description>&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&#34;https://docs.microsoft.com/en-us/vsts/project/wiki/markdown-guidance?view=vsts&#34;&gt;documentation&lt;/a&gt; for the VSTS WIKI suggests you can embed a video in a VSTS WIKI using the markdown/HTML```
&lt;video src=&#34;\_media/vstswiki\_mid.mp4&#34; width=400 controls&gt;
&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Problem is that this does not seem to work, the MP4 just does not appear, you get an empty video player. However, if you swap to a full URL it does work e.g.&lt;/code&gt;
&lt;video src=&#34;https://sec.ch9.ms/ch9/7247/7c8ddc1a-348b-4ba9-ab61-51fded6e7247/vstswiki\_high.mp4&#34; width=400 controls&gt;
&lt;/video&gt;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-This&#34; data-lang=&#34;This&#34;&gt;
### The Workaround

The workaround is to either place the MP4 file in some URL accessible location e.g. some Azure web space (not really addressing the problem), or more usefully use the [VSTS API to get the file](https://docs.microsoft.com/en-us/rest/api/vsts/git/items/get?view=vsts-rest-4.1) out the repo that backs the WIKI. The format of the HTML tag becomes```
&amp;lt;video src=&amp;#34;https://vstsinstance.visualstudio.com/MyTeamProject/\_apis/git/repositories/MyTeamProject.wiki/Items?path=\_media%2Fvstswiki\_high.mp4 width=400&amp;#34; controls&amp;gt;
&amp;lt;/video&amp;gt;
```This will get the current version of the file on default branch, you can add extra parameters to to specify versions and branches if required as per the [API documentation](https://docs.microsoft.com/en-us/rest/api/vsts/git/?view=vsts-rest-4.1). So not a perfect solution as you have to think about branches and versions, they are not handled automatically, but at least it does work
&lt;/code&gt;&lt;/pre&gt;</description>
      <content:encoded><![CDATA[<h3 id="the-problem">The Problem</h3>
<p>The <a href="https://docs.microsoft.com/en-us/vsts/project/wiki/markdown-guidance?view=vsts">documentation</a> for the VSTS WIKI suggests you can embed a video in a VSTS WIKI using the markdown/HTML```
<video src="\_media/vstswiki\_mid.mp4" width=400 controls>
</video></p>
<p><code>Problem is that this does not seem to work, the MP4 just does not appear, you get an empty video player. However, if you swap to a full URL it does work e.g.</code>
<video src="https://sec.ch9.ms/ch9/7247/7c8ddc1a-348b-4ba9-ab61-51fded6e7247/vstswiki\_high.mp4" width=400 controls>
</video></p>
<pre tabindex="0"><code class="language-This" data-lang="This">
### The Workaround

The workaround is to either place the MP4 file in some URL accessible location e.g. some Azure web space (not really addressing the problem), or more usefully use the [VSTS API to get the file](https://docs.microsoft.com/en-us/rest/api/vsts/git/items/get?view=vsts-rest-4.1) out the repo that backs the WIKI. The format of the HTML tag becomes```
&lt;video src=&#34;https://vstsinstance.visualstudio.com/MyTeamProject/\_apis/git/repositories/MyTeamProject.wiki/Items?path=\_media%2Fvstswiki\_high.mp4 width=400&#34; controls&gt;
&lt;/video&gt;
```This will get the current version of the file on default branch, you can add extra parameters to to specify versions and branches if required as per the [API documentation](https://docs.microsoft.com/en-us/rest/api/vsts/git/?view=vsts-rest-4.1). So not a perfect solution as you have to think about branches and versions, they are not handled automatically, but at least it does work
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Registering an agent with VSTS and getting the message &amp;quot;Agent pool not found&amp;quot;</title>
      <link>https://blog.richardfennell.net/posts/registering-an-agent-with-vsts-and-getting-the-message-agent-pool-not-found/</link>
      <pubDate>Thu, 16 Aug 2018 20:58:06 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/registering-an-agent-with-vsts-and-getting-the-message-agent-pool-not-found/</guid>
      <description>&lt;p&gt;When you want to register a build agent with VSTS, you use the VSTS instance’s URL and a user’s Personal Access Token (PAT). Whilst doing this today I connected to the VSTS instance OK but got the error &amp;ldquo;Agent pool not found&amp;rdquo;.when I was asked to pick the agent pool to add the new agent to.&lt;/p&gt;
&lt;p&gt;As the user who’s PAT I was using was a Build Administrator I was a bit confused, but then I remembered to check their user access level. It was set to Stakeholder, once this was changed to Basic I was able to register the agent without use.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>When you want to register a build agent with VSTS, you use the VSTS instance’s URL and a user’s Personal Access Token (PAT). Whilst doing this today I connected to the VSTS instance OK but got the error &ldquo;Agent pool not found&rdquo;.when I was asked to pick the agent pool to add the new agent to.</p>
<p>As the user who’s PAT I was using was a Build Administrator I was a bit confused, but then I remembered to check their user access level. It was set to Stakeholder, once this was changed to Basic I was able to register the agent without use.</p>
<p>Also, so as to not use up a Basic license, when I did not need to, I swapped the user back to being a Stakeholder once the agent was registered. This can be done as the token used for the actual build is not the one used to register it, but one assigned at build time by VSTS.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hello sign in does not work on my first generation Surface Book after a rebuild - fixed</title>
      <link>https://blog.richardfennell.net/posts/hello-sign-in-does-not-work-on-my-first-generation-surface-book-after-a-rebuild-fixed/</link>
      <pubDate>Thu, 02 Aug 2018 16:13:11 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/hello-sign-in-does-not-work-on-my-first-generation-surface-book-after-a-rebuild-fixed/</guid>
      <description>&lt;p&gt;I have just rebuilt my first generation Surface Book from our company standard Windows image. These images are used by all our staff all the time without issue (on Surface and Lenovo devices), so I was not expecting any problems.&lt;/p&gt;
&lt;p&gt;I used to rebuild my PC every 6 months or so, but got out the habit when I moved to a model I could not swap the hard drive out of as backup during the process (using a new disk for new install). I got around this by using &lt;a href=&#34;https://docs.microsoft.com/en-us/sysinternals/downloads/disk2vhd&#34;&gt;Disk2VHD&lt;/a&gt;, not quite as good as I can’t just swap the disk back in, but I won’t have lost any stray files, even though I always aim to keep data in OneDrive, Source Control or SharePoint, so it should not been an issue anyway.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have just rebuilt my first generation Surface Book from our company standard Windows image. These images are used by all our staff all the time without issue (on Surface and Lenovo devices), so I was not expecting any problems.</p>
<p>I used to rebuild my PC every 6 months or so, but got out the habit when I moved to a model I could not swap the hard drive out of as backup during the process (using a new disk for new install). I got around this by using <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/disk2vhd">Disk2VHD</a>, not quite as good as I can’t just swap the disk back in, but I won’t have lost any stray files, even though I always aim to keep data in OneDrive, Source Control or SharePoint, so it should not been an issue anyway.</p>
<p>Anyway, the rebuild went fine, no issues until I tried to enable <a href="https://support.microsoft.com/en-gb/help/17215/windows-10-what-is-hello">Hello</a> to login using the camera. The process seemed to start OK, the wizard ran, but after a reboot there was no Hello login option.</p>
<p>After a bit of digging I found that in device manager there were no imaging devices at all – strange as the Camera App and Skype worked OK.</p>
<p>The Internet proved little help, suggesting the usual set of ‘you have a virus install our tool’ but after much more digging around I found that the cameras were all under ‘System Devices’ in device manager. So I then…</p>
<ol>
<li>Uninstalled them all (front, front IR and back)</li>
<li>Scanned for hardware changes (they reappeared, still in ‘System Devices’</li>
<li>Ran a Windows Update and some new Intel Drivers were downloaded</li>
<li>I could then run the Hello setup wizard again, seems all old settings were lost</li>
</ol>
<p>That was all much more complex than hoped for. My guess is that the system rebuild changed some firmware in a strange way that makes for the misdetection of the cameras.</p>
<p>Anyway it is working now.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Getting Remote Desktop Manager 2.7 working sanely with mixed high DPI screens</title>
      <link>https://blog.richardfennell.net/posts/getting-remote-desktop-manager-2-7-working-sanely-with-mixed-high-dpi-screens/</link>
      <pubDate>Fri, 08 Jun 2018 10:00:52 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/getting-remote-desktop-manager-2-7-working-sanely-with-mixed-high-dpi-screens/</guid>
      <description>&lt;p&gt;&lt;strong&gt;Updated 3 July 2018&lt;/strong&gt; - A colleague, &lt;a href=&#34;https://blogs.blackmarble.co.uk/adavidson/&#34;&gt;Andy Davidson&lt;/a&gt;,  suggested &lt;a href=&#34;https://mremoteng.org/&#34;&gt;mRemoteNG&lt;/a&gt; as an alternative tool to this address this issue. mRemoteNG also has the advantage that it support most major remoting technologies not just RDP, so I am giving that a try for a while. &lt;em&gt;This is one of those post I do mostly for myself so I don’t forget how I did something, it is all based on answers on&lt;/em&gt; &lt;a href=&#34;https://superuser.com/questions/891413/remote-connection-desktop-manager-2-7-does-not-support-dpi-scaling-anymore&#34;&gt;&lt;em&gt;SuperUser.Com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, I can claim no credit&lt;/em&gt; I have a SurfaceBook (first generation) and when I am in the office it is linked to an external monitor, with a different lower DPI, via a dock. If I use Remote Desktop (MSTSC) as built into Windows 10, I can drag sessions between the two monitors and the DPI shift is handled OK. However, if I use my preferred tool &lt;a href=&#34;https://www.microsoft.com/en-gb/download/details.aspx?id=44989&#34;&gt;Remote Desktop Manager 2.7&lt;/a&gt; (as it allow me to store all my commonly used RDP settings) I am in DPI hell. I either get huge fonts or microscopic ones. This is bad whether working on the single high DPI laptop screen work with an external screen. As the &lt;a href=&#34;https://superuser.com/questions/891413/remote-connection-desktop-manager-2-7-does-not-support-dpi-scaling-anymore&#34;&gt;&lt;em&gt;SuperUser.Com&lt;/em&gt;&lt;/a&gt; post states the answer is to change the compatibility settings for the manager by right clicking on the file &amp;ldquo;C:Program Files (x86)MicrosoftRemote Desktop Connection ManagerRDCMan.exe&amp;rdquo;, selecting compatibility, change high DPI settings, and unchecking high DPI setting override &lt;a href=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/06/image.png&#34;&gt;&lt;img alt=&#34;image&#34; loading=&#34;lazy&#34; src=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/06/image_thumb.png&#34; title=&#34;image&#34;&gt;&lt;/a&gt; Once this was done, I have readable resolutions on all screens. Why did I not do a better search months ago?&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><strong>Updated 3 July 2018</strong> - A colleague, <a href="https://blogs.blackmarble.co.uk/adavidson/">Andy Davidson</a>,  suggested <a href="https://mremoteng.org/">mRemoteNG</a> as an alternative tool to this address this issue. mRemoteNG also has the advantage that it support most major remoting technologies not just RDP, so I am giving that a try for a while. <em>This is one of those post I do mostly for myself so I don’t forget how I did something, it is all based on answers on</em> <a href="https://superuser.com/questions/891413/remote-connection-desktop-manager-2-7-does-not-support-dpi-scaling-anymore"><em>SuperUser.Com</em></a><em>, I can claim no credit</em> I have a SurfaceBook (first generation) and when I am in the office it is linked to an external monitor, with a different lower DPI, via a dock. If I use Remote Desktop (MSTSC) as built into Windows 10, I can drag sessions between the two monitors and the DPI shift is handled OK. However, if I use my preferred tool <a href="https://www.microsoft.com/en-gb/download/details.aspx?id=44989">Remote Desktop Manager 2.7</a> (as it allow me to store all my commonly used RDP settings) I am in DPI hell. I either get huge fonts or microscopic ones. This is bad whether working on the single high DPI laptop screen work with an external screen. As the <a href="https://superuser.com/questions/891413/remote-connection-desktop-manager-2-7-does-not-support-dpi-scaling-anymore"><em>SuperUser.Com</em></a> post states the answer is to change the compatibility settings for the manager by right clicking on the file &ldquo;C:Program Files (x86)MicrosoftRemote Desktop Connection ManagerRDCMan.exe&rdquo;, selecting compatibility, change high DPI settings, and unchecking high DPI setting override <a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/06/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/06/image_thumb.png" title="image"></a> Once this was done, I have readable resolutions on all screens. Why did I not do a better search months ago?</p>
]]></content:encoded>
    </item>
    <item>
      <title>A workaround for the error &amp;lsquo;TF14061: The workspace ws_1_18;Project Collection Build Service does not exist&amp;rsquo; when mapping a TFVC workspace</title>
      <link>https://blog.richardfennell.net/posts/a-workaround-for-the-error-tf14061-the-workspace-ws_1_18project-collection-build-service-does-not-exist-when-mapping-a-tfvc-workspace/</link>
      <pubDate>Wed, 30 May 2018 12:17:30 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/a-workaround-for-the-error-tf14061-the-workspace-ws_1_18project-collection-build-service-does-not-exist-when-mapping-a-tfvc-workspace/</guid>
      <description>&lt;p&gt;Whilst writing some training material for VSTS I hit a problem creating a TFVC workspace. I was using VS2017, linking a TFVC Repo to a local folder. I was connecting to the VSTS instance using an MSA.&lt;/p&gt;
&lt;p&gt;In Team Explorer, when I came to do a ‘Map &amp;amp; Get’ to map the source locations I got a ‘TF14061: The workspace ws_1_18;Project Collection Build Service does not exist’ error&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/05/image.png&#34;&gt;&lt;img alt=&#34;image&#34; loading=&#34;lazy&#34; src=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/05/image_thumb.png&#34; title=&#34;image&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Strange error, which I could see no obvious reason for. Turns out the work around was just to press the ‘Advanced’ link/button and accept the defaults&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Whilst writing some training material for VSTS I hit a problem creating a TFVC workspace. I was using VS2017, linking a TFVC Repo to a local folder. I was connecting to the VSTS instance using an MSA.</p>
<p>In Team Explorer, when I came to do a ‘Map &amp; Get’ to map the source locations I got a ‘TF14061: The workspace ws_1_18;Project Collection Build Service does not exist’ error</p>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/05/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/05/image_thumb.png" title="image"></a></p>
<p>Strange error, which I could see no obvious reason for. Turns out the work around was just to press the ‘Advanced’ link/button and accept the defaults</p>
]]></content:encoded>
    </item>
    <item>
      <title>Opps, I made that test VSTS extension public by mistake, what do I do now?</title>
      <link>https://blog.richardfennell.net/posts/opps-i-made-that-test-vsts-extension-public-by-mistake-what-do-i-do-now/</link>
      <pubDate>Sat, 14 Apr 2018 13:50:55 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/opps-i-made-that-test-vsts-extension-public-by-mistake-what-do-i-do-now/</guid>
      <description>&lt;p&gt;I recently, whilst changing a &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/09/major-update-to-my-cicd-process-for-vsts-extensions/&#34;&gt;CI/CD release pipeline&lt;/a&gt;, updated what was previously a private version of a VSTS extension in the &lt;a href=&#34;https://marketplace.visualstudio.com/&#34;&gt;VSTS Marketplace&lt;/a&gt; with a version of the VSIX package set to be public.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note, in my CI/CD process I have a private and public version of each extension (set of tasks), the former is used for functional testing within the CD process, the latter is the one everyone can see&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So, this meant I had two public versions of the same extension, confusing.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I recently, whilst changing a <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/09/major-update-to-my-cicd-process-for-vsts-extensions/">CI/CD release pipeline</a>, updated what was previously a private version of a VSTS extension in the <a href="https://marketplace.visualstudio.com/">VSTS Marketplace</a> with a version of the VSIX package set to be public.</p>
<p><em>Note, in my CI/CD process I have a private and public version of each extension (set of tasks), the former is used for functional testing within the CD process, the latter is the one everyone can see</em>.</p>
<p>So, this meant I had two public versions of the same extension, confusing.</p>
<p>Turns out you can’t change a public extension back to be private, either via the UI or by uploading a corrected VSIX. Also you can’t delete any public extension that has ever been downloaded, and my previously private one had been downloaded once, by me for testing.</p>
<p>So my only option was to un-publish the previously private extension so only the correct version was visible in the public marketplace.</p>
<p>This meant I had to also alter my CI/CD process to change the extensionID of my private extension so I could publish a new private version of the extension.</p>
<p>Luckily, as all the GUIDs for the tasks within the extension did not change once I had installed the new version of the extension I had mispublished in my test VSTS instance my pipeline still worked.</p>
<p>Only downside is I am left with an un-publish ‘dead’ version listed in my private view of the marketplace. This is not a problem, just does not look ‘neat and tidy’</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fix for 2755 and 1632 &amp;lsquo;The Temp folder is on a drive that is full or is inaccessible&amp;rsquo; errors</title>
      <link>https://blog.richardfennell.net/posts/fix-for-2755-and-1632-the-temp-folder-is-on-a-drive-that-is-full-or-is-inaccessible-errors/</link>
      <pubDate>Wed, 21 Mar 2018 17:01:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fix-for-2755-and-1632-the-temp-folder-is-on-a-drive-that-is-full-or-is-inaccessible-errors/</guid>
      <description>&lt;p&gt;Whilst trying to install an MSI package we kept getting the errors 2755 and 1632 ‘The Temp folder is on a drive that is full or is inaccessible’.&lt;/p&gt;
&lt;p&gt;After much fiddling we found the problem was that the &lt;strong&gt;%systemroot%installer&lt;/strong&gt; folder was missing. Once this was manually re-added the MSIs installed without a problem. The actual TEMP folder setting was a red herring&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Whilst trying to install an MSI package we kept getting the errors 2755 and 1632 ‘The Temp folder is on a drive that is full or is inaccessible’.</p>
<p>After much fiddling we found the problem was that the <strong>%systemroot%installer</strong> folder was missing. Once this was manually re-added the MSIs installed without a problem. The actual TEMP folder setting was a red herring</p>
]]></content:encoded>
    </item>
    <item>
      <title>Using VSTS Gates to help improve my deployment pipeline of VSTS Extensions to the Visual Studio Marketplace</title>
      <link>https://blog.richardfennell.net/posts/using-vsts-gates-to-help-improve-my-deployment-pipeline-of-vsts-extensions-to-the-visual-studio-marketplace/</link>
      <pubDate>Tue, 20 Mar 2018 11:30:43 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/using-vsts-gates-to-help-improve-my-deployment-pipeline-of-vsts-extensions-to-the-visual-studio-marketplace/</guid>
      <description>&lt;p&gt;My existing &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/09/major-update-to-my-cicd-process-for-vsts-extensions/&#34;&gt;VSTS CI/CD process&lt;/a&gt; has a problem that the deployment of a VSTS extension, from the moment it is uploaded to when it’s tasks are available to a build agent, is not instantiation. The process can potentially take a few minutes to roll out. The problem this delay causes is a perfect candidate for using &lt;a href=&#34;https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/release/approvals/gates&#34;&gt;VSTS Release Gates&lt;/a&gt;; using the gate to make sure the expected version of a task is available to an agent before running the next stage of the CD pipeline e.g waiting after deploying a private build of an extension before trying to run functional tests. The problem is how to achieve this with the current VSTS gate options?&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>My existing <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/09/major-update-to-my-cicd-process-for-vsts-extensions/">VSTS CI/CD process</a> has a problem that the deployment of a VSTS extension, from the moment it is uploaded to when it’s tasks are available to a build agent, is not instantiation. The process can potentially take a few minutes to roll out. The problem this delay causes is a perfect candidate for using <a href="https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/release/approvals/gates">VSTS Release Gates</a>; using the gate to make sure the expected version of a task is available to an agent before running the next stage of the CD pipeline e.g waiting after deploying a private build of an extension before trying to run functional tests. The problem is how to achieve this with the current VSTS gate options?</p>
<h3 id="what-did-not-work">What did not work</h3>
<p>My first thought was to use the <a href="https://docs.microsoft.com/en-us/vsts/build-release/tasks/utility/http-rest-api">Invoke HTTP REST API gate</a>, calling the VSTS API <strong>https://<your vsts instance name>.visualstudio.com/_apis/distributedtask/tasks/<GUID of Task>.</strong> This API call returns a block of JSON containing details about the deployed task visible to the specified VSTS instance. In theory you can parse this data with a <a href="http://goessner.net/articles/JsonPath/">JSONPATH</a> query in the gates success criteria parameter to make sure the correct version of the task is deployed e.g. <strong>eq($.value[?(@.name == &ldquo;BuildRetensionTask&rdquo;)].contributionVersion, &ldquo;1.2.3&rdquo;)</strong> However, there is a problem. At this time the Invoke HTTP REST API gate task does not support the <strong>==</strong> equality operator in it’s success criteria field. I understand this will be addressed in the future, but the fact it is currently missing is a block to my current needs. Next I thought I could write a custom VSTS gate. These are basically ‘run on server’ tasks with a suitably crafted JSON manifest. The problem here is that this type of task does not allow any code (Node.JS or PowerShell) to be run. They only have a limited capability to invoke HTTP APIs or write messages to service bus. So I could not implement the code I needed to process the API response. So another dead end.</p>
<h3 id="what-did-work">What did work</h3>
<p>The answer, after a suggestion from the VSTS Release Management team at Microsoft, was to try the <a href="https://docs.microsoft.com/en-us/vsts/build-release/tasks/utility/azure-function">Azure Function gate</a>. To do this I created <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-azure-function">a new Azure Function</a>. I did this using the Azure Portal, picking the consumption billing model, C# and securing the function with a function key, basically the default options. I then added <a href="https://github.com/rfennell/vNextBuild/blob/master/AzureFunctions/VSTSExtensionGate/run.csx">the C# function code (stored in GitHub)</a>, to my newly created Azure Function. This function code takes</p>
<ul>
<li>The name of the VSTS instance</li>
<li>A personal access token (PAT) to access the VSTS instance</li>
<li>The GUID of the task to check for</li>
<li>And the version to check for</li>
</ul>
<p>It then returns a JSON block with true or false based on whether the required task version can be found. If any of the parameters are invalid an API error is returned By passing in this set of arguments my idea was that a single Azure Function could be used to check for the deployment of all my tasks. <strong>Note:</strong> Now I do realise I could also create a release pipeline for the Azure Function, but I chose to just create it via the Azure Portal. I know this is not best practice, but this was just a proof of concept. As usual the danger here is that this proof of concept might be one of those that is too useful and lives forever!</p>
<h3 id="to-use-the-azure-function">To use the Azure Function</h3>
<p>Using the Azure function is simple</p>
<ul>
<li>
<ul>
<li>Added an Azure Function <a href="https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/release/approvals/gates">gate to a VSTS release</a></li>
<li>Set the <strong>URL</strong> parameter for the Azure Function. This value can be found from the Azure Portal. Note that you don’t need the Function Code query parameter in the URL as this is provided with the next gate parameter. I chose to use a <a href="https://docs.microsoft.com/en-us/vsts/build-release/concepts/library/variable-groups">variable group variable</a> for this parameter so it was easy to reuse between many CD pipelines</li>
<li>Set the <strong>Function Key</strong> parameter for the Azure Function, again you get this from the Azure Portal. This time I used a secure variable group variable</li>
<li>Set the <strong>Method</strong> parameter to POST</li>
<li>Set the <strong>Header</strong> content type as JSON</li>
</ul>
</li>
</ul>
<pre tabindex="0"><code>{
      &#34;Content-Type&#34;: &#34;application/json&#34;
}
</code></pre><ul>
<li>
<ul>
<li>Set the <strong>Body</strong> to contain the details of the VSTS instance and Task to check. This time I used a mixture of variable group variables, release specific variables (the GUID) and environment build/release variables. The key here is I got the version from the primary release artifact <strong>$(BUILD.BUILDNUMBER)</strong> so the correct version of the tasks is tested for automatically</li>
</ul>
</li>
</ul>
<pre tabindex="0"><code>{
     &#34;instance&#34;: &#34;$(instance)&#34;,
     &#34;pat&#34;: &#34;$(pat)&#34;,
     &#34;taskguid&#34;: &#34;$(taskGuid)&#34;,
     &#34;version&#34;: &#34;$(BUILD.BUILDNUMBER)&#34;
}
</code></pre><ul>
<li>Finally set  the <strong>Advanced/Completion Event</strong> to ApiResponse with the success criteria of```
eq(root[&lsquo;Deployed&rsquo;], &rsquo;true&rsquo;)
<pre tabindex="0"><code></code></pre></li>
</ul>
<p>Once this was done I was able to use the Azure function as a VSTS gate as required</p>
<h3 id="image"><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/03/image-1.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/03/image_thumb-1.png" title="image"></a></h3>
<h3 id="summary">Summary</h3>
<p>So I now have a gate that makes sure that for a given VSTS instance a task of a given version has been deployed. If you need this functionality all you need to do is create your own Azure Function instance, drop in <a href="https://github.com/rfennell/vNextBuild/blob/master/AzureFunctions/VSTSExtensionGate/run.csx">my code</a> and configure the VSTS gate appropriately. When equality == operator becomes available for JSONPATH in the REST API Gate I might consider a swap back to a basic REST call, it is less complex to setup, but we shall see. The Azure function model does appear to work well</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fixing a &amp;lsquo;git-lfs filter-process: gif-lfs: command not found&amp;rsquo; error in Visual Studio 2017</title>
      <link>https://blog.richardfennell.net/posts/fixing-a-git-lfs-filter-process-gif-lfs-command-not-found-error-in-visual-studio-2017/</link>
      <pubDate>Fri, 09 Mar 2018 12:02:17 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/fixing-a-git-lfs-filter-process-gif-lfs-command-not-found-error-in-visual-studio-2017/</guid>
      <description>&lt;p&gt;I am currently looking at the best way to migrate a large legacy codebase from TFVC to Git. There are a number of ways I could do this, as I have &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/05/10/options-migrating-tfs-to-vsts/&#34;&gt;posted about before&lt;/a&gt;. Obviously, I have ruled out anything that tries to migrate history as ‘that way hell lies’; if people need to see history they will be able to look at the archived TFVC instance. TFVC and Git are just too different in the way they work to make history migrations worth the effort in my opinion.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I am currently looking at the best way to migrate a large legacy codebase from TFVC to Git. There are a number of ways I could do this, as I have <a href="https://blogs.blackmarble.co.uk/rfennell/2017/05/10/options-migrating-tfs-to-vsts/">posted about before</a>. Obviously, I have ruled out anything that tries to migrate history as ‘that way hell lies’; if people need to see history they will be able to look at the archived TFVC instance. TFVC and Git are just too different in the way they work to make history migrations worth the effort in my opinion.</p>
<p>So as part of this migration and re-structuring I am looking at using <a href="https://git-scm.com/docs/git-submodule">Git Submodules</a> and <a href="https://git-lfs.github.com/">Git Large File System (LFS)</a> to help divide the monolithic code base into front-end, back-end and shared service modules; using LFS to manage large media files used in integration test cases.</p>
<p>From the PowerShell command prompt, using Git 2.16.2, all my trials were successful, I could achieve what I wanted. However when I tried accessing my trial repos using Visual Studio 2017 I saw issues</p>
<h3 id="submodules">Submodules</h3>
<p>Firstly there are known limitations with Git submodules in Visual Studio Team Explorer. At this time you can clone a repo that has submodules, but you cannot manage the relationships between repos or commit to a submodule from inside Visual Studio.</p>
<p>This is unlike the Git command line, which allows actions to span a parent and child repo with a single command, Git just works it out if <a href="https://git-scm.com/book/en/v2/Git-Tools-Submodules">you pass the right parameters</a></p>
<p>There is a request on <a href="https://visualstudio.uservoice.com/forums/121579-visual-studio-ide/suggestions/8960629-handle-multiple-git-repositories-in-a-visual-studi">UserVoice</a> to add these functions to Visual Studio, vote for it if you think it is important, I have.</p>
<h3 id="large-file-system">Large File System</h3>
<p>The big problem I had was with LFS, which is meant to work in Visual Studio since 2015.2.</p>
<p>Again from the command line operations were seamless, I just installed Git 2.16.2 via <a href="https://chocolatey.org/search?q=git">Chocolaty</a> and got LFS support without installing anything else. So I was able to enable LFS support on a repo</p>
<pre tabindex="0"><code>git lfs install
</code></pre><p>git lfs track &lsquo;*.bin&rsquo;</p>
<pre tabindex="0"><code>git add .gitattributes
```

and manage standard and large (.bin) files without any problems

However, when I tried to make use of this cloned LFS enabled repo from inside Visual Studio by staging a new large .bin file I got an error ‘git-lfs filter-process: gif-lfs: command not found’

[![image](https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/03/image_thumb.png &#34;image&#34;)](https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/03/image.png)

On reading around this error it suggested that the separate [git-lfs package](https://git-lfs.github.com/) needed to be installed. I did this, making sure that the path to the **git-lfs.exe** (C:Program FilesGit LFS) was in my path, but I still had the problem.

This is where I got stuck and hence needed to get some help from the Microsoft Visual Studio support team.

After a good deal tracing they spotted the problem. The path to **git-lfs.exe** was at the end of my rather long PATH list. It seems Visual Studio was truncating this list of paths, so as the error suggested Visual Studio could not find **git-lfs.exe**.

It is unclear to me whether the command prompt just did not suffer this PATH length issue, or was using a different means to resolve LFS feature. It should be noted from the command line LFS commands were available as soon as I installed Git 2.16.2. I did not have to add the Git LFS package.

So the fix was simple, move the entry for ‘C:Program FilesGit LFS’ to the start of my PATH list and everything worked in Visual Studio.

It should be noted I really need to look at whether I need everything in my somewhat long PATH list. It’s been too long since I re-paved my laptop, there is a lot of strange bits installed.

Thanks again to the Visual Studio Support team for getting me unblocked on this.
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Building private VSTS build agents using the Microsoft Packer based agent image creation model</title>
      <link>https://blog.richardfennell.net/posts/building-private-vsts-build-agents-using-the-microsoft-packer-based-agent-image-creation-model/</link>
      <pubDate>Tue, 27 Feb 2018 18:48:32 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/building-private-vsts-build-agents-using-the-microsoft-packer-based-agent-image-creation-model/</guid>
      <description>&lt;h3 id=&#34;background&#34;&gt;Background&lt;/h3&gt;
&lt;p&gt;Having automated builds is essential to any good development process. Irrespective of the build engine in use, VSTS, Jenkins etc. you need to have a means to create the VMs that are running the builds.&lt;/p&gt;
&lt;p&gt;You can of course do this by hand, but in many ways you are just extending the old ‘it works on my PC – the developer can build it only on their own PC’ problem i.e. it is hard to be sure what version of tools are in use. This is made worse by the fact it is too tempting for someone to remote onto the build VM to update some SDK or tool without anyone else’s knowledge.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="background">Background</h3>
<p>Having automated builds is essential to any good development process. Irrespective of the build engine in use, VSTS, Jenkins etc. you need to have a means to create the VMs that are running the builds.</p>
<p>You can of course do this by hand, but in many ways you are just extending the old ‘it works on my PC – the developer can build it only on their own PC’ problem i.e. it is hard to be sure what version of tools are in use. This is made worse by the fact it is too tempting for someone to remote onto the build VM to update some SDK or tool without anyone else’s knowledge.</p>
<p>In an endeavour to address this problem we need a means to create our build VMs in a consistent standardised manner i.e a configuration as code model.</p>
<p><a href="https://blogs.blackmarble.co.uk/rhepworth/2017/03/02/define-once-deploy-everywhere-sort-of/">At Black Marble we have been using Lability</a> to build our lab environments and there is no reason we could not use the same system to create our VSTS build agent VMs</p>
<ul>
<li>Creating base VHDs disk images with patched copies of Windows installed (which we update on a regular basis)</li>
<li>Use <a href="https://github.com/VirtualEngine/Lability">Lability</a> to provision all the required tools – this would need to include all the associated reboots these installers would require. Noting that rebooting and restarting at the correct place, for non DSC based resources, is not Lability’s strongest feature i.e. you have to do all the work in custom code</li>
</ul>
<p>However, there is an alternative. Microsoft have made their <a href="https://www.packer.io/">Packer</a> based method of creating VSTS Azure hosted agents available on <a href="https://github.com/Microsoft/vsts-image-generation">GitHub</a>. Hence, it made sense to me to base our build agent creation system on this standardised image; thus allowing easier migration of builds between private and hosted build agent pools whether in the cloud or on premises, due to the fact they had the same tools installed.</p>
<h3 id="the-basic-process">The Basic Process</h3>
<p>To enable this way of working I <a href="https://github.com/blackmarble/vsts-image-generation">forked the Microsoft repo</a> and modified the Packer JSON configuration file to build Hyper-V based images as opposed to Azure ones. I aimed to make as few changes as possible to ease the process of keeping my forked repo in sync with future changes to the Microsoft standard build agent. In effect replacing the <strong>builder</strong> section of the packer configuration and leaving the <strong>providers</strong> unaltered</p>
<p>So, in doing this I learnt a few things</p>
<h3 id="which-iso-to-use">Which ISO to use?</h3>
<p>Make sure you use a current Operating System ISO. First it save time as it is already patched; but more importantly the provider scripts in the Microsoft configuration assume certain Windows features are available for installation (Containers with Docker support specifically) that were not present on the 2016 RTM ISO</p>
<h3 id="building-an-answeriso">Building an Answer.ISO</h3>
<p>In the <a href="https://www.packer.io/docs/builders/hyperv-iso.html">sample I found</a> for the Packer <strong>hyperv-iso</strong> builder the <strong>AutoUnattended.XML</strong> answers file is provided on an ISO (as opposed to a virtual floppy as floppies are not support on Gen2 HyperV VMs). This means when you edit the answers file you need to rebuild the ISO prior to running Packer.</p>
<p>The sample script to do this has lines to ‘Enable UEFI and disable Non EUFI’; I found that if these lines of PowerShell were run the answers file was ignored on the ISO. I had to comment them out. It seems an <strong>AutoUnattended.XML</strong> answers file edited in VSCode is the correct encoding by default.</p>
<p>I also found that if I ran the PowerShell script to create the ISO from within VSCode’s integrated terminal the ISO builder <strong>mkisofs.exe</strong> failed with an internal error. However, it worked fine from a default PowerShell windows.</p>
<h3 id="installing-the-net-35-feature">Installing the .NET 3.5 Feature</h3>
<p>When a provider tried to install the .NET 3.5 feature using the command</p>
<p><strong>Install-WindowsFeature -Name NET-Framework-Features -IncludeAllSubFeature</strong></p>
<p>it failed.</p>
<p>Seems this is a bug in Windows 2016 and the workaround is to specify the –Source location on the install media</p>
<p><strong>Install-WindowsFeature -Name NET-Framework-Features -IncludeAllSubFeature -Source &ldquo;D:sourcessxs&rdquo;</strong></p>
<p>Once the script was modified in this manner it ran without error</p>
<h3 id="well-how-long-does-it-take">Well how long does it take?</h3>
<p>The Packer process is slow, Microsoft say for an Azure VM it can take up to over 8 hours. A HyperV VM is no faster.</p>
<p>I also found the process a bit brittle. I had to restart the process a good few times as….</p>
<ul>
<li>I ran out of disk space (no unsurprising this broke the process)</li>
<li>The new VM did not get a DHCP assigned IP address when connected to the network via the HyperV Default Switch. A reboot of my HyperV host PC fixed this.</li>
<li>Packer decided the VM had rebooted when it had not – usually due to a slow install of some feature or network issues</li>
<li>My Laptop went to sleep and caused one of the above problems</li>
</ul>
<h3 id="so-i-have-a-sysprepd-vhd-now-what-do-i-do-with-it-now">So I have a SysPrep’d VHD now what do I do with it now?</h3>
<p>At this point I have options of what to do with this new exported HyperV image. I could manually create build agent VM instances.</p>
<p>However, it appeals to me to use this new VHD as a based image for Lability, replacing our default ‘empty patched Operating System’ image creation system, so I have a nice consistent way to provision VMs onto our Hyper-V servers.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Versioning your ARM templates within a VSTS CI/CD pipeline with Semantic Versioning</title>
      <link>https://blog.richardfennell.net/posts/versioning-your-arm-templates-within-a-vsts-ci-cd-pipeline-with-semantic-versioning/</link>
      <pubDate>Sat, 03 Feb 2018 15:23:59 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/versioning-your-arm-templates-within-a-vsts-ci-cd-pipeline-with-semantic-versioning/</guid>
      <description>&lt;p&gt;I wrote a post recently &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2018/01/22/versioning-your-arm-templates-within-a-vsts-ci-cd-pipeline/&#34;&gt;Versioning your ARM templates within a VSTS CI/CD pipeline&lt;/a&gt;. I realised since writing it that it does not address the issue of if you wish to version your ARM Templates using &lt;a href=&#34;https://semver.org/&#34;&gt;Semantic Versioning&lt;/a&gt;. My JSON versioning task I used did not support the option of not extracting a numeric version number e.g. 1.2.3.4 from a VSTS build number. To address this limitation I have modified &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-Versioning-Task&#34;&gt;my Version JSON file task&lt;/a&gt; to address. This change to my task allows it to be used with the &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=gittools.gitversion#overview&#34;&gt;GitVersion VSTS task&lt;/a&gt; to manage the semantic versioning. For more details on &lt;a href=&#34;http://gitversion.readthedocs.io/en/latest/&#34;&gt;GitVersion see the project documentation&lt;/a&gt;. Hence, I my now able to generate a version number using GitVersion and pass this in to the versioning task directly using a build variable.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I wrote a post recently <a href="https://blogs.blackmarble.co.uk/rfennell/2018/01/22/versioning-your-arm-templates-within-a-vsts-ci-cd-pipeline/">Versioning your ARM templates within a VSTS CI/CD pipeline</a>. I realised since writing it that it does not address the issue of if you wish to version your ARM Templates using <a href="https://semver.org/">Semantic Versioning</a>. My JSON versioning task I used did not support the option of not extracting a numeric version number e.g. 1.2.3.4 from a VSTS build number. To address this limitation I have modified <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-Versioning-Task">my Version JSON file task</a> to address. This change to my task allows it to be used with the <a href="https://marketplace.visualstudio.com/items?itemName=gittools.gitversion#overview">GitVersion VSTS task</a> to manage the semantic versioning. For more details on <a href="http://gitversion.readthedocs.io/en/latest/">GitVersion see the project documentation</a>. Hence, I my now able to generate a version number using GitVersion and pass this in to the versioning task directly using a build variable.</p>
<ul>
<li>Add the GitVersion task at the start of the build, with its default parameters</li>
<li>Add my JSON versioning task with default parameters apart from
<ul>
<li><strong>Version Number</strong> set to <strong>$(GitVersion.SemVer)</strong></li>
<li><strong>Use Version Number without Processing (Advanced)</strong> checked</li>
<li><strong>Filename Pattern (Advanced)</strong> set to <strong>azuredeploy.json</strong></li>
<li><strong>Field to update (Advanced)</strong> set to <strong>contentVersion</strong></li>
</ul>
</li>
</ul>
<p><a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/02/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/02/image_thumb.png" title="image"></a> In the logs you see output similar to the following```
Source Directory: E:Build2_work361s
Filename Pattern: azuredeploy.json
Version Number/Build Number: 0.1.0-unstable.843
Use Build Number Directly: true
Version Filter to extract build number: d+.d+.d+.d+
Version Format for JSON File: {1}.{2}.{3}
Field to update (all if empty): contentVersion
Output: Version Number Parameter Name: OutputedVersion
Using the provided build number without any further processing
JSON Version Name will be: 0.1.0-unstable.843
Will apply 0.1.0-unstable.843 to 12 files.
Updating the field &lsquo;contentVersion&rsquo; version
Existing Tag: contentVersion&quot;: &ldquo;1.0.0.0&rdquo;
Replacement Tag: contentVersion&quot;: &ldquo;0.1.0-unstable.843&rdquo;
…</p>
<pre tabindex="0"><code></code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Creating test data for my Generate Release Notes Extension for use in CI/CD process</title>
      <link>https://blog.richardfennell.net/posts/creating-test-data-for-my-generate-release-notes-extension-for-use-in-ci-cd-process/</link>
      <pubDate>Fri, 19 Jan 2018 20:13:38 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/creating-test-data-for-my-generate-release-notes-extension-for-use-in-ci-cd-process/</guid>
      <description>&lt;p&gt;As part of the continued improvement to my CI/CD process I needed to provide a means so that whenever I test my &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-GenerateReleaseNotes-Task&#34;&gt;Generate Release Notes Task&lt;/a&gt;, within it’s CI/CD process, new commits and work item associations are made. This is required because the task only picks up new commits and work items since the last successful running of a given build. So if the last release of the task extension was successful then the next set of tests have no associations to go in the release notes, not exactly exercising all the code paths! In the past I added this test data by hand, a new manual commit to the repo prior to a release; but why have a dog and bark yourself? Better to automate the process. This can done using a PowerShell file, run inline or stored in the builds source repo and run within a VSTS build. The code is shown below, you can pass in the required parameters, but I set sensible default for my purposes&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>As part of the continued improvement to my CI/CD process I needed to provide a means so that whenever I test my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-GenerateReleaseNotes-Task">Generate Release Notes Task</a>, within it’s CI/CD process, new commits and work item associations are made. This is required because the task only picks up new commits and work items since the last successful running of a given build. So if the last release of the task extension was successful then the next set of tests have no associations to go in the release notes, not exactly exercising all the code paths! In the past I added this test data by hand, a new manual commit to the repo prior to a release; but why have a dog and bark yourself? Better to automate the process. This can done using a PowerShell file, run inline or stored in the builds source repo and run within a VSTS build. The code is shown below, you can pass in the required parameters, but I set sensible default for my purposes</p>
<script src="https://gist.github.com/rfennell/2cc36232158518b3b36866bfd321644d.js"></script>
<p>For this PowerShell code to work you do need make some security changes to allow the build agent service user to write to the Git repo. <a href="https://docs.microsoft.com/en-gb/vsts/build-release/actions/scripts/git-commands">This is documented by Microsoft</a>. The PowerShell task to run this code is placed in a build as the only task <a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/01/image.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/01/image_thumb.png" title="image"></a> This build is then triggered as part of the release process <a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/01/image-1.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2018/01/image_thumb-1.png" title="image"></a> Note that the triggering of this build has to be such that it runs on a non-blocking build agent <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/">as discussed in my previous posts</a>. In my case I trigger the build to add the extra commits and work items just before triggering the validation build on my private Azure hosted agent. Now, there is no reason you can’t just run the PowerShell directly within the release if you wanted to. I chose to use a build so that the build could be reused between different VSTS extension CI/CD pipelines; remember I have two Generate Release Note Extensions, PowerShell and NodeJS Based. So another step to fully automating the whole release process.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How I fixed my problem that my VSTS Build Extension was too big to upload to the Marketplace</title>
      <link>https://blog.richardfennell.net/posts/how-i-fixed-my-problem-that-my-vsts-build-extension-was-too-big-to-upload-to-the-marketplace/</link>
      <pubDate>Fri, 05 Jan 2018 14:48:33 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/how-i-fixed-my-problem-that-my-vsts-build-extension-was-too-big-to-upload-to-the-marketplace/</guid>
      <description>&lt;p&gt;Whist adding a couple of new tasks to my &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-Versioning-Task&#34;&gt;VSTS Manifest Versioning Extension&lt;/a&gt; I hit the problem that VSIX package became too big to upload to the Marketplace. The error I saw in my CI/CD VSTS pipeline was```
##vso[task.logissue type=error;]error:
Failed Request: Bad Request(400) -
The extension package size &amp;lsquo;23255292 bytes&amp;rsquo; exceeds the
maximum package size &amp;lsquo;20971520 bytes&amp;rsquo;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-This&#34; data-lang=&#34;This&#34;&gt;
1.  They get a list of files
2.  Extract a version number from the build number
3.  Then apply this to one or more files in a product/task specific manner

there has been some cut and paste coding. This means that I have NPM modules in the tasks **package.json** file that were not needed for a given task. I could manually address this but there is an NPM module to help, DepCheck. First install the [DepCheck](https://www.npmjs.com/package/depcheck) module```
npm install depcheck –g
```then run **depcheck** from the command line whist within your task’s folder. This returns a list of modules listed in the **package.json** that are not referenced in the code files. These can then be removed from the **package.json.**  e.g. I saw```
Unused dependencies
\* @types/node
\* @types/q
\* Buffer
\* fs
\* request
\* tsd
Unused devDependencies
\* @types/chai
\* @types/mocha
\* @types/node
\* mocha-junit-reporter
\* ts-loader
\* ts-node
\* typings
```The important ones to focus on are the first block (non-development references), as these are the ones that are packaged with the production code in the VSIX; I was already pruning the **node\_module** folder of development dependencies prior to creating the VSIX to remove devDependancies using the command```
npm prune –production
```I did find some of the listed modules strange, as I knew they really were needed and a quick test of removing them did show the code failed if they were missing. These are what [depchecks documentation calls false alerts](https://www.npmjs.com/package/depcheck). I found I could remove the **@type/xxx** and **tsd** references, which were the big ones, that are only needed in development when working in TypeScript. Once these were removed for all four of my NodeJS based tasks my VSIX dropped in size from 22Mb to 7Mb. So problem solved.
&lt;/code&gt;&lt;/pre&gt;</description>
      <content:encoded><![CDATA[<p>Whist adding a couple of new tasks to my <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-Versioning-Task">VSTS Manifest Versioning Extension</a> I hit the problem that VSIX package became too big to upload to the Marketplace. The error I saw in my CI/CD VSTS pipeline was```
##vso[task.logissue type=error;]error:
Failed Request: Bad Request(400) -
The extension package size &lsquo;23255292 bytes&rsquo; exceeds the
maximum package size &lsquo;20971520 bytes&rsquo;</p>
<pre tabindex="0"><code class="language-This" data-lang="This">
1.  They get a list of files
2.  Extract a version number from the build number
3.  Then apply this to one or more files in a product/task specific manner

there has been some cut and paste coding. This means that I have NPM modules in the tasks **package.json** file that were not needed for a given task. I could manually address this but there is an NPM module to help, DepCheck. First install the [DepCheck](https://www.npmjs.com/package/depcheck) module```
npm install depcheck –g
```then run **depcheck** from the command line whist within your task’s folder. This returns a list of modules listed in the **package.json** that are not referenced in the code files. These can then be removed from the **package.json.**  e.g. I saw```
Unused dependencies
\* @types/node
\* @types/q
\* Buffer
\* fs
\* request
\* tsd
Unused devDependencies
\* @types/chai
\* @types/mocha
\* @types/node
\* mocha-junit-reporter
\* ts-loader
\* ts-node
\* typings
```The important ones to focus on are the first block (non-development references), as these are the ones that are packaged with the production code in the VSIX; I was already pruning the **node\_module** folder of development dependencies prior to creating the VSIX to remove devDependancies using the command```
npm prune –production
```I did find some of the listed modules strange, as I knew they really were needed and a quick test of removing them did show the code failed if they were missing. These are what [depchecks documentation calls false alerts](https://www.npmjs.com/package/depcheck). I found I could remove the **@type/xxx** and **tsd** references, which were the big ones, that are only needed in development when working in TypeScript. Once these were removed for all four of my NodeJS based tasks my VSIX dropped in size from 22Mb to 7Mb. So problem solved.
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Announcing a new VSTS Extension for Starting and Stopping Azure DevTest Labs VMs</title>
      <link>https://blog.richardfennell.net/posts/announcing-a-new-vsts-extension-for-starting-and-stopping-azure-devtest-labs-vms/</link>
      <pubDate>Thu, 30 Nov 2017 12:13:05 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/announcing-a-new-vsts-extension-for-starting-and-stopping-azure-devtest-labs-vms/</guid>
      <description>&lt;h3 id=&#34;background&#34;&gt;Background&lt;/h3&gt;
&lt;p&gt;I have &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-%3Cpre.vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/&#34;&gt;recently been posting&lt;/a&gt; on using Azure to host private VSTS build/release agents to avoid agent queue deadlocking issues with more complex release pipelines. One of the areas discussed is reducing cost of running a private agent in Azure by only running the private agent within a limited time range, when you guess it might be needed. I have done this using DevTest Labs &lt;a href=&#34;https://azure.microsoft.com/en-gb/updates/azure-devtest-labs-schedule-vm-auto-start/&#34;&gt;Auto Start&lt;/a&gt; and &lt;a href=&#34;https://azure.microsoft.com/en-gb/updates/azure-devtest-labs-set-auto-shutdown-for-a-single-lab-vm/&#34;&gt;Auto Stop&lt;/a&gt; features. This works, but is it not better to only start the agent VM when it is actually really needed, not when you guess it might be? I need this private agent only when working on my VSTS extensions, not something I do everyday. Why waste CPU cycles that are never used?&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h3 id="background">Background</h3>
<p>I have <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-%3Cpre.vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/">recently been posting</a> on using Azure to host private VSTS build/release agents to avoid agent queue deadlocking issues with more complex release pipelines. One of the areas discussed is reducing cost of running a private agent in Azure by only running the private agent within a limited time range, when you guess it might be needed. I have done this using DevTest Labs <a href="https://azure.microsoft.com/en-gb/updates/azure-devtest-labs-schedule-vm-auto-start/">Auto Start</a> and <a href="https://azure.microsoft.com/en-gb/updates/azure-devtest-labs-set-auto-shutdown-for-a-single-lab-vm/">Auto Stop</a> features. This works, but is it not better to only start the agent VM when it is actually really needed, not when you guess it might be? I need this private agent only when working on my VSTS extensions, not something I do everyday. Why waste CPU cycles that are never used?</p>
<h3 id="new-vsts-extension">New VSTS Extension</h3>
<p>I had expected there would already be a VSTS  extension to Start and Stop DevTest Lab VMs, but the <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuredevtestlabs.tasks">Microsoft provided extension for DevTest Labs</a> only provides tasks for the creation and deletion of VMs within a lab. So I am pleased to announce the release of my new <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-DevTestLab">DevTest Labs VSTS Extension</a> to fill this gap, adding tasks to start and stop a DevTest Lab VM on demand from within a build or a release. My Usage I have been able to use the tasks in this extension to start my private Azure hosted agent only when I need it for functional tests within a release. However, they could equally be used for a variety of different testing scenarios where any form of pre-built/configured VMs needs to be started or stopped as opposed to slower processes of creating/deploying a new deployment of a DevTest lab VM. In may case I added an extra agent phases to my release pipeline to start the VM prior to it being needed. <a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2017/11/image-2.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2017/11/image_thumb-2.png" title="image"></a> I could also have used another agent phase to stop the VM once the tests were completed. However, I made the call to leave the VM running and let DevTest Labs’ Auto Stop shut it down at the end of the day. The reason for this is that VM start up and shutdown is still fairly slow, a minute or two, and I often find I need to run a set of function tests a few times during my development cycle; so it is a bit more efficient to leave the VM running until the end of the day. Only taking the start-up cost once. You may have course have different needs, hence providing both the Start and Stop Tasks</p>
<h3 id="development">Development</h3>
<p>This new extension aims to act as a supplement to the <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuredevtestlabs.tasks">Microsoft provided Azure DevTest Lab Extension</a>. Hence to make development and adoption easier, it uses exactly the same <a href="https://github.com/Azure/azure-devtestlab/tree/master/VstsTasks">source code structure</a> and task parameters as the Microsoft provided extension. The task parameters being:</p>
<ul>
<li><strong>Azure RM Subscription</strong> - Azure Resource Manager subscription to configure before running.</li>
<li><strong>Source Lab VM ID</strong> - Resource ID of the source lab VM. The source lab VM must be in the selected lab, as the custom image will be created using its VHD file. You can use any variable such as <em>$(labVMId)</em>, the output of calling Create Azure DevTest Labs VM, that contains a value in the form <em>/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.DevTestLab/labs/{labName}/virtualMachines/{vmName}</em>.</li>
</ul>
<p>The issue I had was that the DevTest Labs PowerShell API did not provide a command to start or stop a VM in a lab. I needed to load the Azure PowerShell library to use the <strong>Invoke-AzureRmResourceAction</strong>  command. This requires you first call <strong>Login-AzureRmAccount</strong> to authenticate prior to calling the actual <strong>Invoke-AzureRmResourceAction</strong> required. This required a bit of extra code to get and reuse the AzureRM endpoint to find the authentication details.```
# Get the parameters
$ConnectedServiceName = Get-VstsInput -Name &ldquo;ConnectedServiceName&rdquo;</p>
<h1 id="get-the-end-point-from-the-name-passed-as-a-parameter">Get the end point from the name passed as a parameter</h1>
<p>$Endpoint = Get-VstsEndpoint -Name $ConnectedServiceName -Require</p>
<h1 id="get-the-authentication-details">Get the authentication details</h1>
<p>$clientID = $Endpoint.Auth.parameters.serviceprincipalid
$key = $Endpoint.Auth.parameters.serviceprincipalkey
$tenantId = $Endpoint.Auth.parameters.tenantid
$SecurePassword = $key | ConvertTo-SecureString -AsPlainText -Force
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $clientID, $SecurePassword</p>
<h1 id="authenticate">Authenticate</h1>
<p>Login-AzureRmAccount -Credential $cred -TenantId $tenantId -ServicePrincipal
<code>Important to note that for this code to work you have to set the task’s **task.json** to run **PowerShell3** and package the Powershell VSTS API module in with the task.</code>
&ldquo;execution&rdquo;: {
&ldquo;PowerShell3&rdquo;: {
&ldquo;target&rdquo;: &ldquo;$(currentDirectory)\StartVM.ps1&rdquo;,
&ldquo;argumentFormat&rdquo;: &ldquo;&rdquo;,
&ldquo;workingDirectory&rdquo;: &ldquo;$(currentDirectory)&rdquo;
    }
  }</p>
<pre tabindex="0"><code class="language-If" data-lang="If">
### In Summary

I have certainly found this extension useful, and I have leant more that I had expect I would about VSTS endpoints and Azure authentication. Hope it is useful to you too.
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Creating a VSTS build agent on an Azure DevLabs Windows Server VM with no GUI  - Using Artifacts</title>
      <link>https://blog.richardfennell.net/posts/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui-using-artifacts/</link>
      <pubDate>Tue, 28 Nov 2017 20:34:10 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui-using-artifacts/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/&#34;&gt;In my last post I discussed creating a private VSTS build agent within an Azure DevTest Lab on a VM with no GUI&lt;/a&gt;. It was pointed out to me today, by &lt;a href=&#34;https://blogs.blackmarble.co.uk/rhepworth&#34;&gt;Rik Hepworth&lt;/a&gt;, that I had overlooked an obvious alternative way to get the VSTS agent onto the VM i.e. not having to use a series of commands at an RDP connected command prompt. The alternative I missed is to use a DevTest Lab Artifact; in fact there is such an artifact available within the standard set in DevTest Labs. You just provide a few parameters and you are good to go. &lt;a href=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2017/11/image-1.png&#34;&gt;&lt;img alt=&#34;image&#34; loading=&#34;lazy&#34; src=&#34;https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2017/11/image_thumb-1.png&#34; title=&#34;image&#34;&gt;&lt;/a&gt; Well you should be good to go, but there is an issue. The PowerShell used to extract the downloaded Build Agent ZIP file does not work on a non-UI based Windows VM. &lt;a href=&#34;http://www.codewrecks.com/blog/index.php/2016/05/27/avoid-using-shell-command-in-powershell-scipts/&#34;&gt;The basic issue here is discussed in this post by my fellow ALM MVP Ricci Gian Maria&lt;/a&gt;. Luckily the fix is simple; I just used the same code to do the extraction of the ZIP file that I used in my &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/&#34;&gt;previous post&lt;/a&gt;. I have submitted this fix as a &lt;a href=&#34;https://github.com/Azure/azure-devtestlab/pull/319&#34;&gt;Pull Request to the DevTest Lab Team&lt;/a&gt; so hopefully the standard repository will have the fix soon and you won’t need to do a fork to create a private artifacts repo as I have. &lt;strong&gt;Update 1st December 2017&lt;/strong&gt; The &lt;a href=&#34;https://github.com/Azure/azure-devtestlab/pull/319&#34;&gt;Pull Request to the DevTest Lab Team&lt;/a&gt; with the fixed code has been accepted and the fix is now in the master branch of the public artifact repo, so automatically available to all&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/">In my last post I discussed creating a private VSTS build agent within an Azure DevTest Lab on a VM with no GUI</a>. It was pointed out to me today, by <a href="https://blogs.blackmarble.co.uk/rhepworth">Rik Hepworth</a>, that I had overlooked an obvious alternative way to get the VSTS agent onto the VM i.e. not having to use a series of commands at an RDP connected command prompt. The alternative I missed is to use a DevTest Lab Artifact; in fact there is such an artifact available within the standard set in DevTest Labs. You just provide a few parameters and you are good to go. <a href="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2017/11/image-1.png"><img alt="image" loading="lazy" src="https://blogs.blackmarble.co.uk/wp-content/uploads/sites/2/2017/11/image_thumb-1.png" title="image"></a> Well you should be good to go, but there is an issue. The PowerShell used to extract the downloaded Build Agent ZIP file does not work on a non-UI based Windows VM. <a href="http://www.codewrecks.com/blog/index.php/2016/05/27/avoid-using-shell-command-in-powershell-scipts/">The basic issue here is discussed in this post by my fellow ALM MVP Ricci Gian Maria</a>. Luckily the fix is simple; I just used the same code to do the extraction of the ZIP file that I used in my <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/23/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/">previous post</a>. I have submitted this fix as a <a href="https://github.com/Azure/azure-devtestlab/pull/319">Pull Request to the DevTest Lab Team</a> so hopefully the standard repository will have the fix soon and you won’t need to do a fork to create a private artifacts repo as I have. <strong>Update 1st December 2017</strong> The <a href="https://github.com/Azure/azure-devtestlab/pull/319">Pull Request to the DevTest Lab Team</a> with the fixed code has been accepted and the fix is now in the master branch of the public artifact repo, so automatically available to all</p>
]]></content:encoded>
    </item>
    <item>
      <title>Creating a VSTS build agent on an Azure DevLabs Windows Server VM with no GUI</title>
      <link>https://blog.richardfennell.net/posts/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/</link>
      <pubDate>Thu, 23 Nov 2017 12:38:48 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui/</guid>
      <description>&lt;p&gt;&lt;strong&gt;Updates&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;28th Nov 2017&lt;/strong&gt;: Also see this second post &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/28/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui-using-artifacts/&#34;&gt;Creating a VSTS build agent on an Azure DevLabs Windows Server VM with no GUI - Using Artifacts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;30th Nov 2017&lt;/strong&gt;: Also see associated post &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/30/announcing-a-new-vsts-extension-for-starting-and-stopping-azure-devtest-labs-vms/&#34;&gt;Announcing a new VSTS Extension for Starting and Stopping Azure DevTest Labs VMs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;As I &lt;a href=&#34;https://blogs.blackmarble.co.uk/rfennell/2017/11/09/major-update-to-my-cicd-process-for-vsts-extensions/&#34;&gt;posted recently&lt;/a&gt; I have been trying to add more functional tests to the VSTS based release CI/CD pipeline for my &lt;a href=&#34;https://marketplace.visualstudio.com/search?term=fennell&amp;amp;target=VSTS&amp;amp;category=All%20categories&amp;amp;sortBy=Relevance&#34;&gt;VSTS Extensions&lt;/a&gt;, and as I noted depending on how you want to run your tests e.g. trigger sub-builds, you can end up with scheduling deadlocks where a single build agent is scheduling the release and trying to run a new build. The answer is to use a second build agent in a different agent pool e.g. if the release is running on the Hosted build agent use a private build agent for the sub-build, or of course just pay for more hosted build instances. The problem with a private build agent is where to run it. As my extensions are a personal project I don’t have a corporate Hyper-V server to run any extra private agents on, as I would have for an company projects. My MVP MSDN Azure benefits are the obvious answer, but I want any agents to be cheap to run, so I don’t burn through all my MSDN credits for a single build agent. To this end I created a Windows Server 2016 VM in &lt;a href=&#34;https://azure.microsoft.com/en-gb/services/devtest-lab/&#34;&gt;DevLabs&lt;/a&gt; (I prefer to create my VMs in DevLabs as it makes it easier tidying up of my Azure account) using an A0 sizing VM. This is tiny so cheap; I don’t intend to ever do a build on this agent, just schedule releases, so need to install few if any tools, so the size should not be an issue. To further reduce costs I used the auto start and stop features on the VM so it is only running during the hours I might be working. So I get an admittedly slow and limited private build agent but for less that $10 a month. As the VM is small it makes sense to not run a GUI. This means when you RDP to the new VM you just get a command prompt. So how do you get the agent onto the VM and setup? You can’t just open a browser to VSTS or cut and paste a file via RDP, and I wanted to avoid the complexity of having to open up PowerShell remoting on the VM. The process I used was as follows:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><strong>Updates</strong></p>
<ul>
<li><strong>28th Nov 2017</strong>: Also see this second post <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/28/creating-a-vsts-build-agent-on-an-azure-devlabs-windows-server-vm-with-no-gui-using-artifacts/">Creating a VSTS build agent on an Azure DevLabs Windows Server VM with no GUI - Using Artifacts</a></li>
<li><strong>30th Nov 2017</strong>: Also see associated post <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/30/announcing-a-new-vsts-extension-for-starting-and-stopping-azure-devtest-labs-vms/">Announcing a new VSTS Extension for Starting and Stopping Azure DevTest Labs VMs</a></li>
</ul>
<hr>
<p>As I <a href="https://blogs.blackmarble.co.uk/rfennell/2017/11/09/major-update-to-my-cicd-process-for-vsts-extensions/">posted recently</a> I have been trying to add more functional tests to the VSTS based release CI/CD pipeline for my <a href="https://marketplace.visualstudio.com/search?term=fennell&amp;target=VSTS&amp;category=All%20categories&amp;sortBy=Relevance">VSTS Extensions</a>, and as I noted depending on how you want to run your tests e.g. trigger sub-builds, you can end up with scheduling deadlocks where a single build agent is scheduling the release and trying to run a new build. The answer is to use a second build agent in a different agent pool e.g. if the release is running on the Hosted build agent use a private build agent for the sub-build, or of course just pay for more hosted build instances. The problem with a private build agent is where to run it. As my extensions are a personal project I don’t have a corporate Hyper-V server to run any extra private agents on, as I would have for an company projects. My MVP MSDN Azure benefits are the obvious answer, but I want any agents to be cheap to run, so I don’t burn through all my MSDN credits for a single build agent. To this end I created a Windows Server 2016 VM in <a href="https://azure.microsoft.com/en-gb/services/devtest-lab/">DevLabs</a> (I prefer to create my VMs in DevLabs as it makes it easier tidying up of my Azure account) using an A0 sizing VM. This is tiny so cheap; I don’t intend to ever do a build on this agent, just schedule releases, so need to install few if any tools, so the size should not be an issue. To further reduce costs I used the auto start and stop features on the VM so it is only running during the hours I might be working. So I get an admittedly slow and limited private build agent but for less that $10 a month. As the VM is small it makes sense to not run a GUI. This means when you RDP to the new VM you just get a command prompt. So how do you get the agent onto the VM and setup? You can’t just open a browser to VSTS or cut and paste a file via RDP, and I wanted to avoid the complexity of having to open up PowerShell remoting on the VM. The process I used was as follows:</p>
<ol>
<li>In VSTS I created a new Agent Pool for my Azure hosted build agents</li>
<li>In the Azure portal, DevLabs I created a new Windows Server 2016 (1709) VM</li>
<li>I then RDP’d to my new Azure VM, in the open Command Prompt I ran PowerShell <strong>powershell</strong></li>
<li>As I was in my users home directory, I  cd’d into the <strong>downloads</strong> folder <strong>cd downloads</strong></li>
<li>I then ran the following PowerShell command to download the agent (you can get the current URI for the agent from your VSTS Agent Pool ‘Download Agent’ feature, but an old version will do as it will auto update. <strong>invoke-webrequest -UseBasicParsing -uri <a href="https://github.com/Microsoft/vsts-agent/releases/download/v2.124.0/vsts-agent-win7-x64-2.124.0.zip">https://github.com/Microsoft/vsts-agent/releases/download/v2.124.0/vsts-agent-win7-x64-2.124.0.zip</a> -OutFile vsts-agent-win7-x64-2.124.0.zip</strong></li>
<li>You can then follow the standard agent setup instructions from the VSTS Agent Pool ‘Download Agent’ feature <strong>mkdir agent ; cd agent PS Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory(&quot;$HOMEDownloadsvsts-agent-win7-x64-2.124.0.zip&quot;, &ldquo;$PWD&rdquo;)</strong></li>
<li>I then configured the agent to run as a service, I exited back to the command prompt to do this this, so the commands were <strong>exit config.cmd</strong></li>
</ol>
<p>I now had an other build agent pool to use in my CI/CD pipelines at a reasonable cost, and the performance was not too bad either.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Moving BM-Bloggers from BlogEngine.NET to WordPress</title>
      <link>https://blog.richardfennell.net/posts/moving-bm-bloggers-from-blogengine-net-to-wordpress/</link>
      <pubDate>Wed, 18 Oct 2017 10:52:28 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/moving-bm-bloggers-from-blogengine-net-to-wordpress/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/rxtur/BlogEngine.NET&#34;&gt;BlogEngine.Net&lt;/a&gt; has served us well as a blogging platform for a good few years. However, it is &lt;a href=&#34;https://github.com/rxtur/BlogEngine.NET/issues/164&#34;&gt;no longer under active support&lt;/a&gt;, so it is time to move on, too much risk of future security issues to ignore the lack of support. After a bit of thought we decided on WordPress as a replacement. OK this has had its own history of problems, but it has an active community and is well supported and in the Azure Marketplace.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/rxtur/BlogEngine.NET">BlogEngine.Net</a> has served us well as a blogging platform for a good few years. However, it is <a href="https://github.com/rxtur/BlogEngine.NET/issues/164">no longer under active support</a>, so it is time to move on, too much risk of future security issues to ignore the lack of support. After a bit of thought we decided on WordPress as a replacement. OK this has had its own history of problems, but it has an active community and is well supported and in the Azure Marketplace.</p>
<p>The <a href="http://laumania.net/2014/10/16/migrating-blogengine-net-wordpress-4-0-2014/">process to move content from BlogEngine to WordPress</a> requires a few steps, and the available community documentation is a bit out of date, mostly due to change in multi-blog support in BlogEngine.NET. So these are the steps I followeded</p>
<h2 id="steps">Steps</h2>
<h3 id="setting-up-a-wordpress-network">Setting up a WordPress Network</h3>
<p>The first step is to create a standard WordPress App Service site on Azure. I used the option to create a MySQL DB in the App Service instance.</p>
<p>Once this was created I needed to make some WordPress setting changes to enable multi blog (network) usage.</p>
<ul>
<li>
<p>First run the standard WordPress setup to create a single site</p>
</li>
<li>
<p>Apply any upgrades available</p>
</li>
<li>
<p>Next I needed to update the settings, this involves editing text files on the instance so I used FTP (Filezilla) and a text editor to edit the required files</p>
</li>
<li>
<p>First I needed to update the php <a href="https://blogs.msdn.microsoft.com/hosamshobak/2015/03/20/azure-websites-how-to-change-php-ini-settings/">execute timeout</a> to give the content import enough time to run for our larger blogs, this means a custom config file. (Actually not sure this is 100% required as the import retry, as discussed below, would probably have been enough)</p>
</li>
<li>
<p>In the Azure portal set the AppSetting <strong>PHP_INI_SCAN_DIR</strong> to <strong>D:homesitewwwroot</strong></p>
</li>
<li>
<p><strong>I</strong>n the root of the site create a text file <strong>phpconfig.ini</strong></p>
</li>
<li>
<p>In the file set <strong>max_execution_time = 600</strong> and save and upload the file</p>
</li>
<li>
<p>As the linked post notes you can check these edit have worked checking with the <strong>phpinfo()</strong>  function on a PHP page on the site.</p>
</li>
<li>
<p>Next <a href="https://codex.wordpress.org/Create_A_Network">create the WordPress network</a>, edit <strong>wp-config.php</strong> and add  <strong>define( &lsquo;WP_ALLOW_MULTISITE&rsquo;, true );</strong></p>
</li>
<li>
<p>Once the site reloads you can now create the network, as the wizard completes it gives instructions to edit the <strong>wp-config.php</strong> and <strong>web.config</strong>, I found note that web.config settings the documentation/wizard gives are missing one element, I needed to use file following</p>
</li>
</ul>
<pre tabindex="0"><code>&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;  
&lt;configuration&gt;
    &lt;system.webServer&gt;
        &lt;rewrite&gt;
            &lt;rules&gt;
                &lt;rule name=&#34;WordPress Rule 1 Identical&#34; stopProcessing=&#34;true&#34;&gt; &lt;match url=&#34;^index.php$&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;action type=&#34;None&#34; /&gt;
                &lt;/rule&gt;
                &lt;rule name=&#34;WordPress Rule 3 Identical&#34; stopProcessing=&#34;true&#34;&gt;
                    &lt;match url=&#34;^(\[\_0-9a-zA-Z-\]+/)?wp-admin$&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;action type=&#34;Redirect&#34; url=&#34;{R:1}wp-admin/&#34; redirectType=&#34;Permanent&#34; /&gt;
                &lt;/rule&gt;
                &lt;rule name=&#34;WordPress Rule 4 Identical&#34; stopProcessing=&#34;true&#34;&gt;
                    &lt;match url=&#34;^&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;conditions logicalGrouping=&#34;MatchAny&#34;&gt;
                        &lt;add input=&#34;{REQUEST\_FILENAME}&#34; matchType=&#34;IsFile&#34; ignoreCase=&#34;false&#34; /&gt;
                        &lt;add input=&#34;{REQUEST\_FILENAME}&#34; matchType=&#34;IsDirectory&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;/conditions&gt;
                    &lt;action type=&#34;None&#34; /&gt;
                &lt;/rule&gt;
                &lt;rule name=&#34;WordPress Rule 5 R2&#34; stopProcessing=&#34;true&#34;&gt;
                    &lt;match url=&#34;^(\[\_0-9a-zA-Z-\]+/)?(wp-(content|admin|includes).\*)&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;action type=&#34;Rewrite&#34; url=&#34;{R:2}&#34; /&gt;
                &lt;/rule&gt;
                &lt;rule name=&#34;WordPress Rule 6 Shorter&#34; stopProcessing=&#34;true&#34;&gt;
                    &lt;match url=&#34;^(\[\_0-9a-zA-Z-\]+/)?(.\*.php)$&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;action type=&#34;Rewrite&#34; url=&#34;{R:2}&#34; /&gt;
                &lt;/rule&gt;
                &lt;rule name=&#34;WordPress Rule 7 Identical&#34; stopProcessing=&#34;true&#34;&gt;
                    &lt;match url=&#34;.&#34; ignoreCase=&#34;false&#34; /&gt;
                    &lt;action type=&#34;Rewrite&#34; url=&#34;index.php&#34; /&gt;
                &lt;/rule&gt;
            &lt;/rules&gt;
        &lt;/rewrite&gt;
    &lt;/system.webServer&gt;
&lt;/configuration&gt; 
</code></pre><p>Once this all done you should have a WordPress network and can start to import the old BlogEngine content into sub sites</p>
<h3 id="create-sites">Create Sites</h3>
<p>Next step is to login as a WordPress network admin (the original account you created in the wizard) and create a sub site.</p>
<p>When you do this a numeric folder will be created in the form <strong>/site/wwwroot/wp-content/uploads/sites/123</strong> , make a note of this number as you need it for fixing the import content in the next step, you might need to create a test post with an image to force this folder creation.</p>
<h3 id="import-the-blogengine-contents">Import the BlogEngine Contents</h3>
<p>Next we needed to import the content our BlogEngine.NET. This is done using the <a href="http://laumania.net/2014/10/16/migrating-blogengine-net-wordpress-4-0-2014/">basic process document in this blog post</a>, but it needs a few updates due to the posts age and the fact we had a multi-blog setup.</p>
<p>The only export option in BlogEbgine.NET is BlogML and if you are running in <a href="https://github.com/rxtur/BlogEngine.NET/issues/92">multisite mode this appears to be broken</a>. The fix is to edit  <strong>/admin/themes/standard/sidebar.cshtml</strong> around line 90 to remove the if test logic blocking showing the export options in multisite mode. Once this is done you can log into to a sub blog and export its contents as a BlogML XML file.</p>
<p><strong>Note:</strong> This is not without its problems, when you are logged into the sub site as an admin and select the <strong>settings&gt;advanced&gt;export</strong> option you get an error as it tries to load the page <a href="http://yoursite/blogml.axd" title="http://localhost/blogs/blogml.axd">http://yoursite/blogml.axd</a>, this is due to the simple hack used to enable the export features, you need to manually edit this URL to <a href="http://yoursite/[blogname]/blogml.axd" title="http://localhost/blogs/blogml.axd">http://yoursite/[blogname]/blogml.axd</a> and the export works OK</p>
<p>You now move the media files associated with the blog posts. The only difference from moving a single blog setup is you need to place them under the <strong>/site/wwwroot/wp-content/uploads/sites/123</strong> previously created. I suggest creating a folder for all the historic post media e.g. <strong>/site/wwwroot/wp-content/uploads/sites/123/historic</strong> and FTPing up all you old images from <strong>blogengine/App_Data/blogs/[name]/files</strong></p>
<p>I next hit the major issues and that is that the BlogML plugin (which you need to install as the WordPress network administrator) is 7 years old, and won’t activate on current versions of WordPress. The issue is changes in the PHP language. The fix is to use the edit option for the plugin and replace all the references to <strong>break $parseBlock</strong> to <strong>break 1</strong> in the file <strong>xpath.class.php.</strong> Once this is done the plugin activates at the network level, so can be used in each sub site</p>
<p>But before we try the import we need to edit exported BlogML file as the blog post says. However, we can improve on the documented process. The blog post says the tags and categories are lost in the import process, this is true for tags, but it is possible to fix the categories. To do this, and fix the images paths, I have written some PowerShell to do the required updates, it is ugly but works opening the file as text and XML separately</p>
<pre tabindex="0"><code> param  
  (  
     $filein = &#34;C:UsersfezDownloadsBlogML (5).xml&#34;,  
     $outfile = &#34;C:UsersfezDownloadsBlogML rfennell.xml&#34;,  
     $blogname = &#34;rfennell&#34;,  
     $blogid = &#34;2&#34;  
  )  
  # Fix the image path  
\[string\]$document = Get-Content -Path $filein

write-output &#34;Replacing name in $oldstring with BlogId $blogid&#34;

$oldstring = &#34;[http://blogs.blackmarble.co.uk/blogs/](http://blogs.blackmarble.co.uk/blogs/)$blogname/image.axd?picture=&#34;  
$document =  $document -Replace \[regex\]::Escape($oldstring) ,&#34;/wp-content/uploads/sites/$blogid/historic/&#34;

#seems to have image URLs with missing slashs in path, take the chance to fix them  
$oldstring = &#34;[http://blogs.blackmarble.co.uk/blogs/](http://blogs.blackmarble.co.uk/blogs/)$($blogname)image.axd?picture=&#34;  
$document =  $document -Replace \[regex\]::Escape($oldstring) ,&#34;/wp-content/uploads/sites/$blogid/historic/&#34;

Set-Content -Value $document -Path $outFile

\[xml\]$XmlDocument = Get-Content -Path $outFile

\# fix the categories block  
foreach ($item in $xmlDocument.blog.categories.category) {  
    try {  
        Write-output &#34;Setting $($item.id) to $($item.title.&#39;#cdata-section&#39;)&#34;  
        # fix all the categories on the post  
        $XmlDocument = $XmlDocument.OuterXml.Replace($($item.id),$($item.title.&#39;#cdata-section&#39;))

        # fix the categories block  
        $item.id = $item.title.&#39;#cdata-section&#39;  
    } catch {}  
  }

$xmlDocument.Save($outfile)  
</code></pre><p>Make sure the parameters are correct for your blog export and target site then process you BlogEngine Export.</p>
<p>You can now login to your newly create WordPress subsite and using the <strong>Tools/Import</strong> option to run the BlogML wizard. You are prompted to remap the authors of the posts as needed. I unified them to the site owner if needed, the started the import. Now we did edit of the PHP timeout, but I found that for my largest 7Mb export file I still got Error 500 timeouts. The good news is that you can just rerun the import (maybe a few time) and it is clever enough to pickup where it left off and will eventually finish, with no duplications. Now there maybe a different timeout you need to set but I did not find it.</p>
<p>You should now have imported post content into you sub site. Unfortunately, you will have to handle static pages manually.  </p>
<h3 id="finishing-up">Finishing Up</h3>
<p>You are now in realm of WordPress, so you can add users, plug-ins and themes as needed to style your set of blogs.</p>
<p>One I found very useful was the <a href="https://en-gb.wordpress.org/plugins/wds-multisite-aggregate/">WDS Multisite Aggregator</a> which allowed the root site to be an aggregation of all the sub sites, just the same as I had on BlogEngine.NET multisite.</p>
<p>Also as I was running on Azure I needed some <a href="https://blogs.msdn.microsoft.com/azureossds/2017/04/20/an-example-of-setting-wordpress-email-with-office-365-smtp/">special handling</a> for email to use SMTP with the plug-in <a href="https://wordpress.org/plugins/wp-mail-smtp/">WP Mail</a>. Once this change was done it could configure the network root site’s email to allow user password resets. For comments each individual sub sites email needs configuring.</p>
<p>I had had concerns over links in old post (as the URL structure had changed), but WordPress seems to sort most of this out, the remained were sorted with redirection rules in the web.config.</p>
<h3 id="conclusion">Conclusion</h3>
<p>This whole process took some experimentation, but once done the rest was a ‘handle turning process’. Lets hope WordPress works as well for us as BlogEngine.NET did in the past.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Putting a release process around my VSTS extension development</title>
      <link>https://blog.richardfennell.net/posts/putting-a-release-process-around-my-vsts-extension-development/</link>
      <pubDate>Fri, 06 May 2016 12:17:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/putting-a-release-process-around-my-vsts-extension-development/</guid>
      <description>&lt;p&gt;&lt;strong&gt;Updated: 5th Aug 2016 added notes in PublisherID&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;  I have been developing a few VSTS/TFS build related extensions and have published a few in the &lt;a href=&#34;https://marketplace.visualstudio.com/search?term=fennell&amp;amp;target=VSTS&amp;amp;sortBy=Relevance&#34;&gt;VSTS marketplace&lt;/a&gt;. This has all been a somewhat manual process, a mixture of Gulp and PowerShell has helped a bit, but I decided it was time to try to do a more formal approach. To do this I have used &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=jessehouwing.jessehouwing-vsts-extension-tasks&#34;&gt;Jesse Houwing’s VSTS Extension Tasks&lt;/a&gt;. Even with this set of tasks I am not sure what I have is ‘best practice’, but it does work. The doubt is due to the way the marketplace handles revisions and preview flags. What I have works for me, but ‘your mileage may differ’&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><strong>Updated: 5th Aug 2016 added notes in PublisherID</strong></p>
<hr>
<p>  I have been developing a few VSTS/TFS build related extensions and have published a few in the <a href="https://marketplace.visualstudio.com/search?term=fennell&amp;target=VSTS&amp;sortBy=Relevance">VSTS marketplace</a>. This has all been a somewhat manual process, a mixture of Gulp and PowerShell has helped a bit, but I decided it was time to try to do a more formal approach. To do this I have used <a href="https://marketplace.visualstudio.com/items?itemName=jessehouwing.jessehouwing-vsts-extension-tasks">Jesse Houwing’s VSTS Extension Tasks</a>. Even with this set of tasks I am not sure what I have is ‘best practice’, but it does work. The doubt is due to the way the marketplace handles revisions and preview flags. What I have works for me, but ‘your mileage may differ’</p>
<h3 id="my-workflow">My Workflow</h3>
<p>The core of my workflow is that I am building the VSIX package twice, once as a private package and the other as a public one. They both contain the same code and have the same version number, they differ in only visibility flags I am not using a the preview flag options at all. I have found they do not really help me. My workflow is to build the private package, upload it and test it by sharing it with a test VSTS instance. if all is good publish the matched public package on the marketplace. In this model there is no need to use a preview, it just adds complexity I don’t need. This may not be true for everyone.</p>
<h3 id="build">Build</h3>
<p>The build’s job is to take the code, set the version number and package it into multiple VSIX package.</p>
<ol>
<li>First I have the vNext build get my source from my GitHub repo.</li>
<li>I add two build variables <strong>$(Major)</strong> and <strong>$(Minor)</strong> that I use to manually manage my version number</li>
<li>I set my build number format to <strong>$(Major).$(Minor).$(rev:r)</strong>, so the final .number is incremented until I choose to increment the major or minor version.</li>
<li>I then use one of Jesse’s tasks to package the extension multiple times using the <strong>extension tag</strong> model parameter. Each different package step uses different <strong>Visibility</strong> settings (circled in red). I also set the version, using the override options, to the <strong>$(Build.BuildNumber)</strong> (circled in green)<a href="/wp-content/uploads/sites/2/historic/image_304.png"><img alt="image" loading="lazy" src="/wp-content/uploads/sites/2/historic/image_thumb_300.png" title="image"></a></li>
<li>**[Updated Aug 2016] **Set the PublisherID and ExtensionID on the tasks, using a pair of build variables is a good idea here to avoid entering strings twice. It is important thay the PublisherID is entered with the correct case - it is case sensitive within the marketplace. Strange things happend of the PublisherID in a VSIX package differ from the one registered on the marketplace</li>
<li>As I am using the VSTS hosted build agent I also need to make sure I check the <strong>install Tfx-cli</strong> in the global setting section</li>
<li>I then add a second identical publish task, but this time there is no tag set and the visibility is set to public.</li>
<li>Finally I use a ‘publish build artifacts’ task to copy the VSIX packages to a drop location</li>
</ol>
<h3 id="release">Release</h3>
<p>So now I have multiple VSIX packages I can use the same family of tasks to create a release pipeline. I create a new release linked to be a Continuous Deployment of the previously created build and set its release name format to <strong>Release-$(Build.BuildNumber)</strong> My first environment uses three tasks, all using the option - to work from a VSIX package. <strong>Note</strong> In all cases I am using the VSIX path in the format <strong>$(System.DefaultWorkingDirectory)/GenerateReleaseNotes.Master/vsix/<package name>-<tag>-$(Build.BuildNumber).vsix</strong>. I am including the build number variable in the path as I chose to put all the packages in a single folder, so path wildcards are not an option as the task would not know which package to use unless I alter my build to put one VSIX package per folder. My tasks for the first environment are</p>
<ol>
<li>Publish VSTS Extension – using my private package so it is added as a private package to the marketplace</li>
<li>Share VSTS Extension – to my test VSTS account</li>
<li>Install VSTS Extension – to my test VSTS account</li>
</ol>
<p><em>For details in the usage of these tasks and setting up the link to the VSTS Marketplace</em> <a href="https://github.com/jessehouwing/vsts-extension-tasks/wiki"><em>see Jesse’s wiki</em></a> If I only intend a extension to ever be private this is enough. However I want to make mine public so I add a second environment that has manual pre-approval (so I have to confirm the public release) This environment only needs single task</p>
<ol>
<li>Publish VSTS Extension – using my public package so it is added as a public package to the marketplace</li>
</ol>
<p>I can of course add other tasks to this environment maybe send a Tweet or email to publicise the new version’s release</p>
<h3 id="summary">Summary</h3>
<p>So now I have a formal way to release my extensions. The dual packaging model means I can publish two different versions at the same time one privately and the other public <a href="/wp-content/uploads/sites/2/historic/image_305.png"><img alt="image" loading="lazy" src="/wp-content/uploads/sites/2/historic/image_thumb_301.png" title="image"></a> It is now just a case of moving all my extensions over to the new model. Though I am still interested to hear what other people view are? Does this seem a reasonable process flow?</p>
]]></content:encoded>
    </item>
    <item>
      <title>Running Pester PowerShell tests in the VSTS hosted build service</title>
      <link>https://blog.richardfennell.net/posts/running-pester-powershell-tests-in-the-vsts-hosted-build-service/</link>
      <pubDate>Sun, 21 Feb 2016 23:43:00 +0000</pubDate>
      <guid>https://blog.richardfennell.net/posts/running-pester-powershell-tests-in-the-vsts-hosted-build-service/</guid>
      <description>&lt;p&gt;**Updated 22 Mar 2016 **This task is available in the &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-PesterRunner-Task&#34;&gt;VSTS Marketplace&lt;/a&gt; If you are using &lt;a href=&#34;https://github.com/pester/Pester/wiki&#34;&gt;Pester&lt;/a&gt; to unit test your PowerShell code then there is a good chance you will want to include it in your automated build process. To do this, you need to get Pester installed on your build machine. The usual options would be&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manual install from &lt;a href=&#34;https://github.com/pester/Pester&#34;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install via &lt;a href=&#34;https://chocolatey.org/packages/pester&#34;&gt;Chocolaty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install via &lt;a href=&#34;https://www.nuget.org/packages/Pester/&#34;&gt;Nuget&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you own the build agent VM then any of these options are good, you can even write the NuGet restore into your build process itself. However there is a problem, both the first two options need administrative access as they put the Pester module in the &lt;strong&gt;$PSModules&lt;/strong&gt; folder (under ‘Program Files’); so these can’t be used on VSTS’s hosted build system, where your are not an administrator So this means you are left with copying the module (and associated functions folder) to some local working folder and running it manually; but do you really want to have to store the Pester module in your source repo? My solution was to write a vNext build tasks to deploy the Pester files and run the Pester tests. &lt;a href=&#34;https://blog.richardfennell.net/wp-content/uploads/sites/2/historic/image_thumb[12].png&#34;&gt;&lt;img alt=&#34;image_thumb[12]&#34; loading=&#34;lazy&#34; src=&#34;https://blog.richardfennell.net/wp-content/uploads/sites/2/historic/image_thumb[12]_thumb.png&#34; title=&#34;image_thumb[12]&#34;&gt;&lt;/a&gt; The task takes two parameters&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>**Updated 22 Mar 2016 **This task is available in the <a href="https://marketplace.visualstudio.com/items?itemName=richardfennellBM.BM-VSTS-PesterRunner-Task">VSTS Marketplace</a> If you are using <a href="https://github.com/pester/Pester/wiki">Pester</a> to unit test your PowerShell code then there is a good chance you will want to include it in your automated build process. To do this, you need to get Pester installed on your build machine. The usual options would be</p>
<ul>
<li>Manual install from <a href="https://github.com/pester/Pester">GitHub</a></li>
<li>Install via <a href="https://chocolatey.org/packages/pester">Chocolaty</a></li>
<li>Install via <a href="https://www.nuget.org/packages/Pester/">Nuget</a></li>
</ul>
<p>If you own the build agent VM then any of these options are good, you can even write the NuGet restore into your build process itself. However there is a problem, both the first two options need administrative access as they put the Pester module in the <strong>$PSModules</strong> folder (under ‘Program Files’); so these can’t be used on VSTS’s hosted build system, where your are not an administrator So this means you are left with copying the module (and associated functions folder) to some local working folder and running it manually; but do you really want to have to store the Pester module in your source repo? My solution was to write a vNext build tasks to deploy the Pester files and run the Pester tests. <a href="/wp-content/uploads/sites/2/historic/image_thumb[12].png"><img alt="image_thumb[12]" loading="lazy" src="/wp-content/uploads/sites/2/historic/image_thumb[12]_thumb.png" title="image_thumb[12]"></a> The task takes two parameters</p>
<ul>
<li>The root folder to look for test scripts with the naming convention  <strong>*.tests.ps1</strong>. Defaults to <strong>$(Build.SourcesDirectory)*</strong></li>
<li>The results file name, defaults to <strong>$(Build.SourcesDirectory)Test-Pester.XML</strong></li>
</ul>
<p>The Pester task does not in itself upload the test results, it just throws and error if tests fails. It relies on the standard test results upload task. Add this task and set</p>
<ul>
<li>it to look for nUnit format files</li>
<li>it already defaults to the correct file name pattern.</li>
<li>IMPORTANT: As the Pester task will stop the build on an error you need to set the ‘Always run’ to make sure the results are published.</li>
</ul>
<p><a href="/wp-content/uploads/sites/2/historic/image_thumb[11].png"><img alt="image_thumb[11]" loading="lazy" src="/wp-content/uploads/sites/2/historic/image_thumb[11]_thumb.png" title="image_thumb[11]"></a> Once all this is added to your build you can see your Pester test results in the build summary <a href="/wp-content/uploads/sites/2/historic/image_thumb[10].png"><img alt="image_thumb[10]" loading="lazy" src="/wp-content/uploads/sites/2/historic/image_thumb[10]_thumb.png" title="image_thumb[10]"></a> <a href="/wp-content/uploads/sites/2/historic/image_thumb[14].png"><img alt="image_thumb[14]" loading="lazy" src="/wp-content/uploads/sites/2/historic/image_thumb[14]_thumb.png" title="image_thumb[14]"></a> You can find the task in my <a href="https://github.com/rfennell/vNextBuild">vNextBuild repo</a></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
