<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-AU" xmlns:media="http://search.yahoo.com/mrss/">
  <id>https://david.gardiner.net.au/tags/GitHub%20Actions.xml</id>
  <title type="html">David Gardiner - GitHub Actions</title>
  <updated>2026-05-23T00:34:35.892Z</updated>
  <subtitle>Blog posts tagged with &apos;GitHub Actions&apos; - A blog of software development, .NET and other interesting things</subtitle>
  <rights>Copyright 2026 David Gardiner</rights>
  <icon>https://www.gravatar.com/avatar/37edf2567185071646d62ba28b868fab?s=64</icon>
  <logo>https://www.gravatar.com/avatar/37edf2567185071646d62ba28b868fab?s=256</logo>
  <generator uri="https://github.com/flcdrg/astrojs-atom" version="3">astrojs-atom</generator>
  <author>
    <name>David Gardiner</name>
  </author>
  <link href="https://david.gardiner.net.au/tags/GitHub%20Actions.xml" rel="self" type="application/atom+xml"/>
  <link href="https://david.gardiner.net.au/tags/GitHub%20Actions" rel="alternate" type="text/html" hreflang="en-AU"/>
  <category term="GitHub Actions"/>
  <category term="Software Development"/>
  <entry>
    <id>https://david.gardiner.net.au/2025/06/migrating-to-astro-quality</id>
    <updated>2025-06-10T08:00:00.000+09:30</updated>
    <title>Migrating my blog to Astro - Quality</title>
    <link href="https://david.gardiner.net.au/2025/06/migrating-to-astro-quality" rel="alternate" type="text/html" title="Migrating my blog to Astro - Quality"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2025-06-10T08:00:00.000+09:30</published>
    <summary type="html">Enhancing the continuous integration and static analysis tooling to keep the quality of
scripts and content as high as possible for the website.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;This is part of a &lt;a href=&quot;/2025/06/migrating-from-jekyll-to-astro#other-posts-in-this-series&quot;&gt;series of posts&lt;/a&gt; on how I migrated my blog to Astro&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/astro-logo-dark.Cy5TuUje_keRSU.webp&quot; alt=&quot;Astro logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There were a few things I put in place, or updated to improve the quality of the code and content of my blog.&lt;/p&gt;
&lt;h2&gt;Lychee link checking&lt;/h2&gt;
&lt;p&gt;I&apos;ve &lt;a href=&quot;/2022/04/blog-fix-part2&quot;&gt;blogged previously about using Lychee to find and fix broken links on your website&lt;/a&gt;. I figured this was a good time to revisit that process and see if it could be updated.&lt;/p&gt;
&lt;p&gt;It turns out that &lt;a href=&quot;https://lychee.cli.rs/&quot;&gt;Lychee&lt;/a&gt; recently changed their output format, so some of my GitHub Actions were no longer working. I&apos;ve now fixed the &lt;a href=&quot;https://github.com/marketplace/actions/wayback-machine-query&quot;&gt;Wayback machine query&lt;/a&gt; GitHub Action so it is compatible with Lychee 0.18 and above.&lt;/p&gt;
&lt;p&gt;Here&apos;s an extract from my &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/.github/workflows/main.yml#L331-L378&quot;&gt;main GitHub workflow&lt;/a&gt; that runs Lychee:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  linkChecker:
    runs-on: ubuntu-latest
    needs: build
    name: Link Checker
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # Cache lychee results (e.g. to avoid hitting rate limits)
      - name: Restore lychee cache
        uses: actions/cache@v4
        with:
          path: .lycheecache
          key: cache-lychee-${{ github.sha }}
          restore-keys: cache-lychee-

      - name: Download dist artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Link Checker
        id: lychee
        uses: lycheeverse/lychee-action@v2.4.1 # 2.4.0 has a bug. Don&apos;t upgrade until fixed
        with:
          fail: false
          output: ${{ github.workspace }}/output.json
          format: json
          token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
          args: --root-dir &quot;$(pwd)/dist/&quot; &apos;dist/**/*.html&apos; --cache --max-cache-age 7d --max-retries 0 --user-agent &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0&quot; --fallback-extensions html -v --cache-exclude-status &apos;429&apos; --insecure --accept &apos;200..=204&apos;

      - name: Report results
        run: |
          ./Generate-LycheeReport.ps1 -InputJsonFile output.json -OutputMarkdownFile output.md

          ls -al

          cat output.md

          cat output.md &amp;gt;&amp;gt; $env:GITHUB_STEP_SUMMARY
        shell: pwsh

      - name: Publish results
        uses: actions/upload-artifact@v4
        with:
          name: link-checker-results
          path: output.*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I created a PowerShell script &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/Generate-LycheeReport.ps1&quot;&gt;&lt;/a&gt; to generate a simple Markdown report that I could then output as a GitHub Actions summary.&lt;/p&gt;
&lt;p&gt;The data file is also published as a build artifact.&lt;/p&gt;
&lt;p&gt;I converted the &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/.github/workflows/links.yml&quot;&gt;links workflow&lt;/a&gt; so that it will now use that artifact as the source for creating a pull request with fixes for broken links where there is a match on archive.org.&lt;/p&gt;
&lt;h2&gt;Empty frontmatter&lt;/h2&gt;
&lt;p&gt;Sometimes I&apos;d uncomment some of the frontmatter properties in a blog post, but forget to replace the placeholder text.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/post-with-placeholder-text.N5iUY2rs_Z1pUXjp.webp&quot; alt=&quot;Example showing placeholder text was not changed&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Whoops!&lt;/p&gt;
&lt;p&gt;So I added an &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/.github/workflows/main.yml#L41&quot;&gt;additional job&lt;/a&gt; to the main GitHub Actions workflow to validate frontmatter properties.&lt;/p&gt;
&lt;h2&gt;Lighthouse&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse&quot;&gt;Lighthouse&lt;/a&gt; is a popular tool for evaluating web page performance and accessibility.&lt;/p&gt;
&lt;p&gt;I added a job to my GitHub Actions workflow to run a &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/.github/workflows/main.yml#L224&quot;&gt;few key pages against Lighthouse&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The job publishes a GitHub step summary to the build, and links are provided if you want to drill into review the analysis of each page tested.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/lighthouse-summary.tIPAhoZt_1eGLdq.webp&quot; alt=&quot;Screenshot of Lighthouse summary&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The results are generally very good. The main thing bring down the blog post page score is because it includes the Disqus commenting addin. One day I&apos;ll see if there&apos;s better alternative to that which plays nicer with the way it interacts with your web pages.&lt;/p&gt;
&lt;h2&gt;Markdownlint&lt;/h2&gt;
&lt;p&gt;I make regular use of David Anson&apos;s &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint&quot;&gt;Markdownlint extension for VS Code&lt;/a&gt;, and so it made sense that I incorporate checking the entire set of Markdown files with a &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/.github/workflows/lint.yml&quot;&gt;separate GitHub Actions workflow&lt;/a&gt;. I did have to do a bit of work to clean up some residual issues in older posts, but they&apos;re all fixed now and with this in place things should stay in good shape.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/astro-logo-dark.Cy5TuUje.png" width="920" height="320"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/astro-logo-dark.Cy5TuUje.png" width="920" height="320"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2024/03/au-dotnet</id>
    <updated>2024-03-31T17:00:00.000+10:30</updated>
    <title>Automatic updating of Chocolatey packages with .NET</title>
    <link href="https://david.gardiner.net.au/2024/03/au-dotnet" rel="alternate" type="text/html" title="Automatic updating of Chocolatey packages with .NET"/>
    <category term=".NET"/>
    <category term="Chocolatey"/>
    <category term="GitHub Actions"/>
    <category term="PowerShell"/>
    <published>2024-03-31T17:00:00.000+10:30</published>
    <summary type="html">How to automate Chocolatey package updates with .NET tooling as a replacement for the AU PowerShell module.</summary>
    <content type="html">&lt;p&gt;I maintain quite a few &lt;a href=&quot;https://community.chocolatey.org/profiles/flcdrg&quot;&gt;Chocolatey packages&lt;/a&gt;. The source for these packages lives in &lt;a href=&quot;https://github.com/flcdrg/au-packages/&quot;&gt;https://github.com/flcdrg/au-packages/&lt;/a&gt;, and until recently I used the &lt;a href=&quot;https://www.powershellgallery.com/packages/AU/2022.10.24&quot;&gt;AU PowerShell module&lt;/a&gt; to detect and publish updated versions of the packages.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/chocolatey-logo.q-2VblLS_1Bkbou.webp&quot; alt=&quot;Chocolatey logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The first issue was that unfortunately, the original maintainer of the AU module archived the project on GitHub. The Chocolatey Community stepped in and is now maintaining a fork &lt;a href=&quot;https://github.com/chocolatey-community/chocolatey-au&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second issue I hit that was causing issues was a compatibility issue with newer versions of PowerShell 7. AU was originally written for Windows PowerShell 5, but I have made extensive use of features of PowerShell 6 and 7 in my update scripts. That didn&apos;t seem to cause issues until the GitHub Actions agents were &lt;a href=&quot;https://github.com/actions/runner-images/issues/9115&quot;&gt;updated from PowerShell 7.2 to 7.4 in January&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The specific problem would reveal itself like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Chocolatey had an error occur: System.ArgumentException: File specified is either not found or not a .nupkg file. &apos;D:\a\au-packages\au-packages\microsoft-teams.install\microsoft-teams.install.1.7.0.3653.nupkg &apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For some reason, the AU module was able to generate a new version of a package, but when it called the Chocolatey CLI (&lt;code&gt;choco.exe&lt;/code&gt;) and passed the path to the nupkg file, it appeared that there was a trailing space in the filename!&lt;/p&gt;
&lt;p&gt;I spent hours trying to debug this to no avail. This was not made any easier by the fact that AU uses &lt;a href=&quot;https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_jobs?view=powershell-7.4&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;PowerShell Jobs&lt;/a&gt; to spin up separate processes for each package so they can be processed in parallel. I could not get debugging to work inside a Job when using the Visual Studio Code PowerShell debugger. Even the old-style debugging approach of &lt;code&gt;Write-Host &quot;I got here&quot;&lt;/code&gt; didn&apos;t work very well as all output of the job is captured isn&apos;t easy to extract (let alone being able to inspect the original variables as proper objects rather than serialised strings)&lt;/p&gt;
&lt;p&gt;Eventually, I decided I was wasting my time trying to solve this, and maybe if I rewrote the updating logic myself I could mitigate the issue.&lt;/p&gt;
&lt;p&gt;There are essentially two parts to the AU module - the bits that support updating an individual package, and then there are the bits that run over all your packages. It&apos;s that second part that makes use of PowerShell Jobs and I suspected was the source of the problem.&lt;/p&gt;
&lt;p&gt;I figured rewriting that part in C#/.NET would mean I had a much nicer debugging experience (should I need it). I wanted to leave the individual package updating alone - it would be a significant effort to migrate all the custom &lt;code&gt;update.ps1&lt;/code&gt; scripts to something else.&lt;/p&gt;
&lt;h2&gt;au-dotnet&lt;/h2&gt;
&lt;p&gt;And so &lt;a href=&quot;https://github.com/flcdrg/au-dotnet&quot;&gt;au-dotnet&lt;/a&gt; was born.&lt;/p&gt;
&lt;p&gt;It is a reasonably simple .NET 8 console application that iterates over all the packages in my au-packages repository, and then calls the PowerShell &lt;code&gt;update.ps1&lt;/code&gt; script in each to see if there is a new version to generate and publish.&lt;/p&gt;
&lt;p&gt;Rather than just call out to the operating system to run each &lt;code&gt;update.ps1&lt;/code&gt; script, I decided to embed PowerShell in the application. This gives me a bit more control over how the scripts are run and the ability to capture any script output (and errors) from each run.&lt;/p&gt;
&lt;h3&gt;Hosting PowerShell&lt;/h3&gt;
&lt;p&gt;Figuring out how to host PowerShell in a .NET 8 application took a little bit of research. Many of the articles you find (and even some of the official documentation) are still aimed at Windows PowerShell.&lt;/p&gt;
&lt;p&gt;The key was to reference these three NuGet packages (and use the same version of each package):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft.PowerShell.Commands.Diagnostics&lt;/li&gt;
&lt;li&gt;Microsoft.PowerShell.SDK&lt;/li&gt;
&lt;li&gt;System.Management.Automation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can then create a &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/system.management.automation.powershell?view=powershellsdk-7.4.0&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;PowerShell Class&lt;/a&gt; instance like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var iss = InitialSessionState.CreateDefault2();
iss.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.RemoteSigned;

var ps = PowerShell.Create(iss);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can capture any output via the &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/system.management.automation.powershell.streams?view=powershellsdk-7.4.0&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;Streams&lt;/code&gt;&lt;/a&gt; property. eg. Here I am logging any errors from PowerShell as a GitHub Action error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ps.Streams.Error.DataAdded += (_, args) =&amp;gt;
{
    core.WriteError(ps.Streams.Error[args.Index].ToString());
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running specific PowerShell cmdlets can be done via the &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/system.management.automation.powershell.addcommand?view=powershellsdk-7.4.0&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;AddCommand&lt;/code&gt;&lt;/a&gt; method. eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ps.AddCommand(&quot;Set-Location&quot;).AddParameter(&quot;Path&quot;, directory).Invoke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whereas running arbitrary PowerShell scripts is done via the &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/system.management.automation.powershell.addscript?view=powershellsdk-7.4.0&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;AddScript&lt;/code&gt;&lt;/a&gt; method. eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ps.AddScript(&quot;$ErrorView = &apos;DetailedView&apos;&quot;).Invoke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the script is in a separate &lt;code&gt;.ps1&lt;/code&gt; file, the only way I&apos;ve found so far is to load that file into a string and pass it in. It would be nicer if you could point it at the file (so debugging/errors could include line numbers) but I have yet to find a way to do that.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var output = ps.AddScript(File.ReadAllText(Path.Combine(directory, &quot;update.ps1&quot;)))
.Invoke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to remember is you must call the &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/system.management.automation.powershell.invoke?view=powershellsdk-7.4.0&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;Invoke&lt;/code&gt;&lt;/a&gt; method to actually run the scripts or commands you&apos;ve just added.&lt;/p&gt;
&lt;h3&gt;GitHub Action logging and summary&lt;/h3&gt;
&lt;p&gt;Because I know the application will be run in a GitHub Actions workflow, I made use of the excellent &lt;a href=&quot;https://www.nuget.org/packages/GitHub.Actions.Core&quot;&gt;GitHub.Actions.Core&lt;/a&gt; NuGet package for formatting output, as well as generating a nice build summary that lists all packages that were updated in the current run.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/build-summary.BBYF1cs__Z18vxyw.webp&quot; alt=&quot;Screenshot of GitHub Actions build summary, showing 17 packages updated and a table with the package names and versions&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Commit and publish&lt;/h3&gt;
&lt;p&gt;If a new package is created (eg. a &lt;code&gt;.nupkg&lt;/code&gt; file now exists) then we assume this file can be submitted to the Chocolatey Community Repository. &lt;code&gt;choco push&lt;/code&gt; is then called to upload the package. Remember this was where we hit that error with the trailing space? Pleasingly the .NET version doesn&apos;t exhibit this behaviour, so that problem is solved.&lt;/p&gt;
&lt;p&gt;Assuming the package is submitted successfully then we call `git`` to stage any modified files from this package and add a tag indicating the package that was updated.&lt;/p&gt;
&lt;p&gt;After all packages have been processed, we will commit all staged files and push the commit back to the repo, so that we get a version history of all the package changes.&lt;/p&gt;
&lt;h3&gt;Enhancements&lt;/h3&gt;
&lt;p&gt;I am currently using my fork of the chocolatey-au module, which has one minor enhancement. It adds a &lt;code&gt;Files&lt;/code&gt; collection property to the &lt;code&gt;AUPackage&lt;/code&gt; PowerShell class. This collection is populated with the paths of all the files that were downloaded (and had their checksums calculated).&lt;/p&gt;
&lt;p&gt;I make use of this for some of my packages to pre-emptively upload the files to VirusTotal. This can help fast-track the packages being approved by Chocolatey as it means the Chocolatey virus scanning step is already completed. Because I use the &lt;a href=&quot;https://community.chocolatey.org/packages/vt-cli&quot;&gt;VirusTotal CLI&lt;/a&gt; tool for this, it also means I can upload files &lt;a href=&quot;https://github.com/VirusTotal/vt-cli/issues/33#issuecomment-850213255&quot;&gt;up to 650MB&lt;/a&gt; (compared to Chocolatey&apos;s current 200MB limit due to using an older API).&lt;/p&gt;
&lt;p&gt;I have &lt;a href=&quot;https://github.com/chocolatey-community/chocolatey-au/pull/53&quot;&gt;submitted the Files property enhancement&lt;/a&gt; to chocolatey-au.&lt;/p&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;You can see this in action in the latest workflow runs at &lt;a href=&quot;https://github.com/flcdrg/au-packages/actions&quot;&gt;https://github.com/flcdrg/au-packages/actions&lt;/a&gt;.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/chocolatey-logo.q-2VblLS.png" width="403" height="275"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/chocolatey-logo.q-2VblLS.png" width="403" height="275"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/12/agent-changes</id>
    <updated>2022-12-06T18:00:00.000+10:30</updated>
    <title>Important changes for Azure DevOps Pipeline agents and GitHub Actions runners</title>
    <link href="https://david.gardiner.net.au/2022/12/agent-changes" rel="alternate" type="text/html" title="Important changes for Azure DevOps Pipeline agents and GitHub Actions runners"/>
    <category term="Azure Pipelines"/>
    <category term="GitHub Actions"/>
    <published>2022-12-06T18:00:00.000+10:30</published>
    <summary type="html">I don&apos;t think this has been publicised as widely as it should, especially for Azure Pipelines consumers.</summary>
    <content type="html">&lt;p&gt;I don&apos;t think this has been publicised as widely as it should, especially for Azure Pipelines consumers. &lt;code&gt;ubuntu-latest&lt;/code&gt; is now resolving to &lt;code&gt;ubuntu-22.04&lt;/code&gt; rather than &lt;code&gt;ubuntu-20.04&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This has been mentioned in the &lt;a href=&quot;https://github.blog/changelog/2022-11-09-github-actions-ubuntu-latest-workflows-will-use-ubuntu-22-04/&quot;&gt;GitHub blog&lt;/a&gt; and there is an &lt;a href=&quot;https://github.com/actions/runner-images/issues/6399&quot;&gt;&apos;announcement&apos; issue in the runner-images repo&lt;/a&gt;, but I haven&apos;t seen anything official for Azure DevOps/Azure Pipelines. Both services use the same agent virtual machine images. As of this writing, the &lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/agents/hosted?view=azure-devops&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;Microsoft-hosted agent page&lt;/a&gt; still suggests that &lt;code&gt;ubuntu-20.04&lt;/code&gt; is used.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/actions/runner-images/issues/6399&quot;&gt;This issue&lt;/a&gt; summarises the software differences between 20.04 and 22.04, and they can be significant. For example, I&apos;ve already seen builds failing because they were assuming a .NET Core 3.1 SDK was preinstalled. The 22.04 image only includes .NET 6 and 7.&lt;/p&gt;
&lt;p&gt;This is in addition to the &lt;a href=&quot;https://learn.microsoft.com/azure/devops/release-notes/2022/pipelines/sprint-209-update?tabs=yaml&amp;amp;WT.mc_id=DOP-MVP-5001655#updated-brownout-schedule-for-ubuntu-1804-images&quot;&gt;already announced removal&lt;/a&gt; of &lt;code&gt;ubuntu-18.04&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So make sure you declare all your tool requirements in your pipelines. Even better, run your jobs in a container (&lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/process/container-phases?view=azure-devops&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;Azure Pipelines&lt;/a&gt; or &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container&quot;&gt;GitHub Actions&lt;/a&gt;) with a custom container image that precisely specifies the tools and versions required to build and deploy your application.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/07/azure-pipelines-container-jobs</id>
    <updated>2022-07-20T09:00:00.000+09:30</updated>
    <title>Building and using Azure Pipelines Container Jobs</title>
    <link href="https://david.gardiner.net.au/2022/07/azure-pipelines-container-jobs" rel="alternate" type="text/html" title="Building and using Azure Pipelines Container Jobs"/>
    <category term="Azure Pipelines"/>
    <category term="GitHub Actions"/>
    <published>2022-07-20T09:00:00.000+09:30</published>
    <summary type="html">The big advantage of using self-hosted build agents with a build pipeline is that you can benefit from installing specific tools and libraries, as well as caching packages between builds.</summary>
    <content type="html">&lt;p&gt;The big advantage of using self-hosted build agents with a build pipeline is that you can benefit from installing specific tools and libraries, as well as caching packages between builds.&lt;/p&gt;
&lt;p&gt;This can also be a big disadvantage - for example, if a build before yours decided to install Node version 10, but your build assumes you have Node 14, then you&apos;re in for a potentially nasty surprise when you add an NPM package that specifies it requires Node &amp;gt; 10.&lt;/p&gt;
&lt;p&gt;An advantage of Microsoft-hosted build agents (for Azure Pipelines or GitHub Actions) is every build job gets a fresh build environment. While they do come with &lt;a href=&quot;https://github.com/actions/runner-images&quot;&gt;pre-installed software&lt;/a&gt;, you&apos;re free to modify and update to your requirements, without fear that you&apos;ll impact any subsequent build jobs.&lt;/p&gt;
&lt;p&gt;The downside of these agents is that any prerequisites you need for your build have to be installed each time your build job runs. The &lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/cache-v2?view=azure-pipelines&amp;amp;viewFallbackFrom=azure-devops&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;Cache task&lt;/a&gt; might help with efficiently restoring packages, but it probably won&apos;t have much impact on installing your toolchain.&lt;/p&gt;
&lt;p&gt;This can be where &lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/process/container-phases?view=azure-devops&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;Container Jobs&lt;/a&gt; come into play.&lt;/p&gt;
&lt;p&gt;Whilst this post focuses on Azure Pipelines, GitHub Actions also supports &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/run-jobs-in-a-container&quot;&gt;running jobs in a container&lt;/a&gt; too, so the same principles will apply.&lt;/p&gt;
&lt;h2&gt;Container jobs&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Containers provide isolation from the host and allow you to pin specific versions of tools and dependencies. Host jobs require less initial setup and infrastructure to maintain.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You could use a public Docker image, like &lt;code&gt;ubuntu:20.04&lt;/code&gt;, or you could create your custom image that includes additional prerequisites.&lt;/p&gt;
&lt;p&gt;To make a job into a container job, you add a &lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/yaml-schema/jobs-job-container?view=azure-pipelines&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;container&lt;/code&gt;&lt;/a&gt;, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;container: ubuntu:20.04

steps:
- script: printenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this in place, when the job starts, the image is automatically downloaded, a container using the image is started, and (by default) all the steps for the job are run in the context of the container.&lt;/p&gt;
&lt;p&gt;Note that it&apos;s now up to you to ensure that the image you&apos;ve selected for your container job has all the tools required by all the tasks in the job. For example, the &lt;code&gt;ubuntu:20.04&lt;/code&gt; image doesn&apos;t include PowerShell, so if I had tried to use a &lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/powershell-v2?view=azure-pipelines&amp;amp;viewFallbackFrom=azure-devops&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;PowerShell@2&lt;/code&gt; task&lt;/a&gt;, it would fail (as it couldn&apos;t find &lt;code&gt;pwsh&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;If the particular toolchain you require is closely tied to the source code you&apos;re building (eg. C# 10 source code will require at least .NET 6 SDK), then I think there&apos;s a strong argument for versioning the toolchain alongside your source code. But if your toolchain definition is in the same source code repository as your application code, how do you efficiently generate the toolchain image that can be used to build the application?&lt;/p&gt;
&lt;p&gt;The approach I&apos;ve adopted is to have a pipeline with two jobs. The first job is just responsible for maintaining the toolchain image (aka building the Docker image).&lt;/p&gt;
&lt;p&gt;The second job uses this Docker image (as a container job) to provide the environment used to build the application.&lt;/p&gt;
&lt;p&gt;Building the toolchain image can be time-consuming. I want my builds to finish as quickly as possible. It would be ideal if we could somehow completely skip (or minimise) the work for this if there were no changes to the toolchain since the last time we built it.&lt;/p&gt;
&lt;p&gt;I&apos;ve applied two techniques to help with this:&lt;/p&gt;
&lt;h2&gt;-cache-from&lt;/h2&gt;
&lt;p&gt;If your agent is self-hosted, then Docker builds can benefit from layer caching. A Microsoft-hosted agent is new for every build, so there&apos;s no layer cache from the previous build.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build&lt;/code&gt; has a &lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources&quot;&gt;&lt;code&gt;-cache-from&lt;/code&gt; option&lt;/a&gt;. This allows you to reference another image that may have layers that be used as a cache source. The ideal cache source for an image would be the previous version of the same image.&lt;/p&gt;
&lt;p&gt;For an image to be used as a cache source, it needs extra metadata included in the image when it is created. This is done by adding a build argument &lt;code&gt;--build-arg BUILDKIT_INLINE_CACHE=1&lt;/code&gt;, and ensuring we use BuildKit by defining an environment variable &lt;code&gt;DOCKER_BUILDKIT: 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s the build log showing how because the layers in the cache were a match, they were used rather than the related commands in the Dockerfile needing to be re-executed.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#4 importing cache manifest from ghcr.io/***/azure-pipelines-container-jobs:latest
#4 sha256:3445b7dd16dde89921d292ac2521908957fc490da1e463b78fcce347ed21c808
#4 DONE 0.9s

#5 [2/2] RUN apk add --no-cache --virtual .pipeline-deps readline linux-pam   &amp;amp;&amp;amp; apk --no-cache add bash sudo shadow   &amp;amp;&amp;amp; apk del .pipeline-deps   &amp;amp;&amp;amp; apk add --no-cache icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib   &amp;amp;&amp;amp; apk add --no-cache libgdiplus --repository https://dl-3.alpinelinux.org/alpine/edge/testing/   &amp;amp;&amp;amp; apk add --no-cache     ca-certificates     less     ncurses-terminfo-base     tzdata     userspace-rcu     curl   &amp;amp;&amp;amp; curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -Version 6.0.302 -InstallDir /usr/share/dotnet     &amp;amp;&amp;amp; ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet   &amp;amp;&amp;amp; apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache     lttng-ust   &amp;amp;&amp;amp; curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.2.5/powershell-7.2.5-linux-alpine-x64.tar.gz -o /tmp/powershell.tar.gz   &amp;amp;&amp;amp; mkdir -p /opt/microsoft/powershell/7   &amp;amp;&amp;amp; tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7   &amp;amp;&amp;amp; chmod +x /opt/microsoft/powershell/7/pwsh   &amp;amp;&amp;amp; ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh   &amp;amp;&amp;amp; pwsh --version
#5 sha256:675dec3a2cb67748225cf1d9b8c87ef1218f51a4411387b5e6a272ce4955106e
#5 pulling sha256:43a07455240a0981bdafd48aacc61d292fa6920f16840ba9b0bba85a69222156
#5 pulling sha256:43a07455240a0981bdafd48aacc61d292fa6920f16840ba9b0bba85a69222156 4.6s done
#5 CACHED

#8 exporting to image
#8 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#8 exporting layers done
#8 writing image sha256:7b0a67e798b367854770516f923ab7f534bf027b7fb449765bf26a9b87001feb done
#8 naming to ghcr.io/***/azure-pipelines-container-jobs:latest done
#8 DONE 0.0s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Skip if nothing to do&lt;/h2&gt;
&lt;p&gt;A second thing we can do is figure out if we actually need to rebuild the image in the first place. If the files in the directory where the Dockerfile is located haven&apos;t changed, then we can just skip all the remaining steps in the job!&lt;/p&gt;
&lt;p&gt;This is calculated by a PowerShell script, originally from this &lt;a href=&quot;https://gist.github.com/Myrddraal/f5a84cf3242e3b3804fa727005ed2786&quot;&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Working example&lt;/h2&gt;
&lt;p&gt;I&apos;ve created a sample project at &lt;a href=&quot;https://github.com/flcdrg/azure-pipelines-container-jobs&quot;&gt;https://github.com/flcdrg/azure-pipelines-container-jobs&lt;/a&gt; that demonstrates this approach in action.&lt;/p&gt;
&lt;p&gt;In particular, note the following files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/flcdrg/azure-pipelines-container-jobs/blob/main/azure-pipelines.yml&quot;&gt;azure-pipelines.yml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/flcdrg/azure-pipelines-container-jobs/blob/main/build/containers/Dockerfile&quot;&gt;Dockerfile&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some rough times I saw for the &apos;Build Docker Image&apos; job:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Time (seconds)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Initial build&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental change&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Additional considerations&lt;/h2&gt;
&lt;p&gt;The sample project works nicely, but if you did want to use this in a project that made use of pull request builds, you might want to adjust the logic slightly.&lt;/p&gt;
&lt;p&gt;Docker images with the &lt;code&gt;latest&lt;/code&gt; version should be preserved for use with &lt;code&gt;main&lt;/code&gt; builds, or PR builds that don&apos;t modify the image.&lt;/p&gt;
&lt;p&gt;A PR build that includes changes to the Docker image should use that modified image, but no other builds should use that image until it has been merged into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Is this right for me?&lt;/h2&gt;
&lt;p&gt;Measuring the costs and benefits of this approach is essential. If the time spent building the image, as well as the time to download the image for the container job exceeds the time to build without a container, then using a container job for performance alone doesn&apos;t make sense.&lt;/p&gt;
&lt;p&gt;Even if you&apos;re adopting container jobs primarily for the isolation they bring to your builds, it&apos;s useful to understand any performance impacts on your build times.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In this post, we saw how container jobs can make a build pipeline more reliable, and potential improvements in time to build completion.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/azure-pipelines-logo.B45UakAg.png" width="80" height="80"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/azure-pipelines-logo.B45UakAg.png" width="80" height="80"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/04/blog-fix-part6</id>
    <updated>2022-04-24T09:00:00.000+09:30</updated>
    <title>Fixing my blog (part 6) - Accessibility scanning</title>
    <link href="https://david.gardiner.net.au/2022/04/blog-fix-part6" rel="alternate" type="text/html" title="Fixing my blog (part 6) - Accessibility scanning"/>
    <category term="Accessibility"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2022-04-24T09:00:00.000+09:30</published>
    <summary type="html">Now we&apos;ve got broken links sorted, we&apos;re in a better state to start accessibility testing using the Accessibility Insights Action.</summary>
    <content type="html">&lt;p&gt;Now we&apos;ve got broken links sorted, we&apos;re in a better state to start accessibility testing using the &lt;a href=&quot;https://github.com/microsoft/accessibility-insights-action&quot;&gt;Accessibility Insights Action&lt;/a&gt;. This is available as both a GitHub Action and an Azure DevOps extension. I&apos;ll be using the GitHub Action version.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Accessibility

on:
  workflow_dispatch:
    
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - uses: ./.github/actions/jekyll-build-pages
        with:
          verbose: false

      - run: |
          sudo find -type f ! -regex &quot;.*\.\(html\|svg\|gif\|css\|jpg\|png\)&quot; -delete
        name: Remove non-HTML
        
        # https://github.com/microsoft/accessibility-insights-action
      - name: Scan for accessibility issues
        uses: microsoft/accessibility-insights-action@v2
        with:
            repo-token: ${{ secrets.GITHUB_TOKEN }}
            site-dir: ${{ github.workspace }}/_site
            scan-timeout: 6000000
            #max-urls: 1500
            localhost-port: 12345
            scan-url-relative-path: /

      - name: Upload report artifact
        uses: actions/upload-artifact@v2
        with:
            name: accessibility-reports
            path: ${{ github.workspace }}/_accessibility-reports/index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The action can either scan a local directory or a URL. In my case, I want it to scan all the files that make up my blog. My blog content is written in Markdown (.md) files and uses the Jekyll engine to render those pages into .html. It&apos;s the latter that should be scanned for accessibility compliance.&lt;/p&gt;
&lt;p&gt;To generate the .html files, I make use of the &lt;a href=&quot;https://github.com/actions/jekyll-build-pages&quot;&gt;Jekyll-Build-Pages&lt;/a&gt; action. This will generate a bunch of files under the &lt;code&gt;_site&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;My initial testing with the scanning tool revealed it was triggering on some pages I was including but had no control over (eg. Google Ads and the Disqus comments). I wanted to exclude those from the scanning, and one way to do that is to make the content conditional. eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{%- if jekyll.environment == &quot;production&quot; -%}
&amp;lt;script async src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-999999999&quot; crossorigin=&quot;anonymous&quot;&amp;gt;&amp;lt;/script&amp;gt;
{%- endif -%}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To exclude this block, I then needed to ensure that &lt;code&gt;jekyll.environment&lt;/code&gt; was not set to &lt;code&gt;production&lt;/code&gt;. I achieved this by using a local copy of the action in which I set the value of the &lt;code&gt;JEKYLL_ENV&lt;/code&gt; environment variable to &lt;code&gt;development&lt;/code&gt;. To facilitate that I need to make a few other changes &lt;a href=&quot;https://github.com/flcdrg/jekyll-build-pages/commit/cb39bdab8a6d15cd9da8c80a4cb44171c4ba352c&quot;&gt;which you can see on this branch&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To help focus the scanning just on the files I care about, I added an extra step to the workflow to remove any files that weren&apos;t one of .html, .svg, .gif, .jpg or .png.&lt;/p&gt;
&lt;p&gt;Depending on how many files you have in your website, scanning can take quite.&lt;/p&gt;
&lt;p&gt;It&apos;s probably a good idea to not set &lt;code&gt;max-urls&lt;/code&gt; the first time. This uses the default of 100, which will be enough to give you an idea of the kinds of problems you need to fix.&lt;/p&gt;
&lt;p&gt;The reason for starting small is if you have a template or CSS that are used across every page, then every page will trigger errors, and your scan will take ages to finish and contains heaps of the same error(s).&lt;/p&gt;
&lt;p&gt;Once you&apos;ve resolved those common errors, then you can ramp up your &lt;code&gt;max-urls&lt;/code&gt; to cover all the pages on your site (if it didn&apos;t already).&lt;/p&gt;
&lt;p&gt;Running the workflow above also produces an &apos;Accessibility Checks&apos; report. You can use this to get a quick overview of the results. To drill in to the details, you should download the build artifact view the &lt;code&gt;index.html&lt;/code&gt; file in your browser.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/accessibility-action-report.DMjd5TDg_24jDGU.webp&quot; alt=&quot;Accessibility report screenshot&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Fixing the errors&lt;/h2&gt;
&lt;p&gt;The scan flagged 5 rule violations. Expanding each rule lists the URLs that exhibited the problem, plus a suggestion on how to resolve the issue. Sometime there are multiple suggestions.&lt;/p&gt;
&lt;h3&gt;color-contrast: Ensure the contrast between foreground and background colors meet WCAG 2 AA contrast ration thresholds&lt;/h3&gt;
&lt;p&gt;Example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;a href=&quot;/tag/Family.html&quot;&amp;gt;Family&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fix the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Element has insufficient color contrast of 4.14 (foreground color: #2a7ae2, background color: #fdfdfd, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1Element has insufficient color contrast of 4.14 (foreground color: #2a7ae2, background color: #fdfdfd, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use foreground color: #2773d6 and the original background color: #fdfdfd to meet a contrast ratio of 4.58:1.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Element has insufficient color contrast of 3.77 (foreground color: #828282, background color: #fdfdfd, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1Element has insufficient color contrast of 3.77 (foreground color: #828282, background color: #fdfdfd, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use foreground color: #747474 and the original background color: #fdfdfd to meet a contrast ratio of 4.59:1.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;These were common across all pages, so fixing these early is a quick win. I searched for the hex color (usually it was defined in one of the .scss files that generate the CSS) and replaced it with the suggested colour&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://accessibilityinsights.io/info-examples/web/link-name/&quot;&gt;link-name&lt;/a&gt;: Ensures links have discernible text&lt;/h3&gt;
&lt;p&gt;Fix the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Element is in tab order and does not have accessible text&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fix ONE of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Element does not have text that is visible to screen readers&lt;/li&gt;
&lt;li&gt;aria-label attribute does not exist or is empty&lt;/li&gt;
&lt;li&gt;aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty&lt;/li&gt;
&lt;li&gt;Element has no title attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://accessibilityinsights.io/info-examples/web/image-alt/&quot;&gt;image-alt&lt;/a&gt;: Ensures &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements have alternate text or a role of none or presentation&lt;/h3&gt;
&lt;p&gt;Fix ONE of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Element does not have an alt attribute&lt;/li&gt;
&lt;li&gt;aria-label attribute does not exist or is empty&lt;/li&gt;
&lt;li&gt;aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty&lt;/li&gt;
&lt;li&gt;Element has no title attribute&lt;/li&gt;
&lt;li&gt;Element&apos;s default semantics were not overridden with role=&quot;none&quot; or role=&quot;presentation&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a href=&quot;https://accessibilityinsights.io/info-examples/web/html-has-lang/&quot;&gt;html-has-lang&lt;/a&gt;: Ensure every HTML document has a lang attribute&lt;/h3&gt;
&lt;p&gt;Fix the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element does not have a lang attribute&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;This was a false positive as for some reason it was being flagged on images.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;https://accessibilityinsights.io/info-examples/web/frame-title/&quot;&gt;frame-title&lt;/a&gt;: Ensures &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;frame&amp;gt;&lt;/code&gt; elements have an accessible name&lt;/h3&gt;
&lt;p&gt;Fix ONE of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Element has no title attribute&lt;/li&gt;
&lt;li&gt;aria-label attribute does not exist or is empty&lt;/li&gt;
&lt;li&gt;aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty&lt;/li&gt;
&lt;li&gt;Element&apos;s default semantics were not overridden with role=&quot;none&quot; or role=&quot;presentation&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;This was being flagged by some older YouTube embedded player HTML.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve raise a bug to report the problem with links to images being flagged. It feels to me like the scanner is trying to scan image URLs (which it shouldn&apos;t). Whether that&apos;s a bug in the action, or in how I&apos;m using it, I hope to find out soon.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/04/blog-fix-part5</id>
    <updated>2022-04-23T09:00:00.000+09:30</updated>
    <title>Fixing my blog (part 5) - Putting it all together with a PR</title>
    <link href="https://david.gardiner.net.au/2022/04/blog-fix-part5" rel="alternate" type="text/html" title="Fixing my blog (part 5) - Putting it all together with a PR"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2022-04-23T09:00:00.000+09:30</published>
    <summary type="html">In part 4 of this series of posts we used the Replace multiple strings in files GitHub Action to update files with the replacement values.</summary>
    <content type="html">&lt;p&gt;In &lt;a href=&quot;/2022/04/blog-fix-part4&quot;&gt;part 4 of this series of posts&lt;/a&gt; we used the &lt;a href=&quot;https://github.com/marketplace/actions/replace-multiple-strings-in-files&quot;&gt;&lt;code&gt;Replace multiple strings in files&lt;/code&gt;&lt;/a&gt; GitHub Action to update files with the replacement values. The files are modified on disk, but what to do with the changes? I could just automatically commit the changes back to version control, but I prefer to take a more cautious approach and give myself a chance to review the changes to confirm if they look reasonable. Creating a pull request is a great way of doing that.&lt;/p&gt;
&lt;p&gt;A an action I&apos;ve used successfully before to do this is Peter Evans&apos; &lt;a href=&quot;https://github.com/marketplace/actions/create-pull-request&quot;&gt;&lt;code&gt;Create Pull Request&lt;/code&gt;&lt;/a&gt; GitHub Action. Using it is very easy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Create Pull Request
  uses: peter-evans/create-pull-request@v4.0.2
  with:
    token: ${{ secrets.PAT_REPO_FULL }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I pass in a personal access token so that any workflows that should be run on creation of a pull request will be executed.&lt;/p&gt;
&lt;p&gt;And with that, I have a full workflow for checking for broken links and repairing them. The only thing to watch out for is that issue I mentioned previously with the invalid JSON produced by the Lychee action. As a temporary measure, I inlined that action in my repo and applied a local code fix. Hopefully once the Lychee action itself is updated then I can go back to using their implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Links

on:
  workflow_dispatch:
    
jobs:
  linkChecker:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Link Checker
        id: lychee
        uses: ./.github/actions/lychee-action #lycheeverse/lychee-action@v1.4.1
        env:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
        with:
          args: &apos;--verbose ./_posts/**/*.md --exclude-mail --scheme http https&apos;
          format: json
          output: ./lychee/links.json
          fail: false

      - uses: actions/upload-artifact@v3.0.0
        with:
          path: ./lychee/links.json

      - name: Wayback Machine Query
        uses: flcdrg/wayback-machine-query-action@v2
        id: wayback
        with:
          source-path: ./lychee/links.json
          timestamp-regex: &apos;_posts\/(\d+)\/(?&amp;lt;year&amp;gt;\d+)-(?&amp;lt;month&amp;gt;\d+)-(?&amp;lt;day&amp;gt;\d+)-&apos;

      - uses: actions/upload-artifact@v3.0.0
        with:
          path: ./wayback/replacements.json
          
      - name: Replacements
        uses: flcdrg/replace-multiple-action@v1
        with:
          find: ${{ steps.wayback.outputs.replacements }}
          prefix: &apos;(^|\\s+|\\()&apos;
          suffix: &apos;($|\\s+|\\))&apos;
          
      - name: Create Pull Request
        # if: ${{ github.ref == &apos;refs/heads/main&apos; }}
        uses: peter-evans/create-pull-request@v4.0.2
        with:
          token: ${{ secrets.PAT_REPO_FULL }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we&apos;ve addressed the broken links, we&apos;re in a better state to revisit the &lt;a href=&quot;https://github.com/microsoft/accessibility-insights-action&quot;&gt;Accessibility Insights Action&lt;/a&gt; that started this whole adventure!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/04/blog-fix-part4</id>
    <updated>2022-04-22T09:00:00.000+09:30</updated>
    <title>Fixing my blog (part 4) - Updating the files</title>
    <link href="https://david.gardiner.net.au/2022/04/blog-fix-part4" rel="alternate" type="text/html" title="Fixing my blog (part 4) - Updating the files"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2022-04-22T09:00:00.000+09:30</published>
    <summary type="html">Last time we looked at using the Wayback Machine Query GitHub Action to automate querying the Wayback Machine for all our broken links.</summary>
    <content type="html">&lt;p&gt;&lt;a href=&quot;/2022/04/blog-fix-part3&quot;&gt;Last time&lt;/a&gt; we looked at using the &lt;a href=&quot;https://github.com/marketplace/actions/wayback-machine-query&quot;&gt;&lt;code&gt;Wayback Machine Query GitHub Action&lt;/code&gt;&lt;/a&gt; to automate querying the Wayback Machine for all our broken links. Now we need to apply the changes to our files.&lt;/p&gt;
&lt;p&gt;Ideally there&apos;d be a GitHub Action that could take a list of our changes and apply them to a list of files. I did search for something like that but all I could find were actions that only made one single change. Time to create another action.&lt;/p&gt;
&lt;p&gt;Enter the &lt;a href=&quot;https://github.com/marketplace/actions/replace-multiple-strings-in-files&quot;&gt;&lt;code&gt;Replace multiple strings in files&lt;/code&gt;&lt;/a&gt; GitHub Action.&lt;/p&gt;
&lt;p&gt;For example, to find all instances of &apos;Multiple&apos; and replace them with &apos;Many&apos; for all the .md files in the current directory you can do:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- uses: flcdrg/replace-multiple-action@v1
  with:
    files: &apos;./*.md&apos;
    find: &apos;[{ &quot;find&quot;: &quot;Multiple&quot;, &quot;replace&quot;: &quot;Many&quot; }]&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For my case I want something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- uses: flcdrg/replace-multiple-action@v1
  with:
    files: &apos;./*.md&apos;
    find: &apos;[{ &quot;find&quot;: &quot;http://localhost&quot;, &quot;replace&quot;: &quot;https://localhost&quot;}, { &quot;find&quot;: &quot;http://davidgardiner.net.au&quot;, &quot;replace&quot;: &quot;https://david.gardiner.net.au&quot; }]&apos;
    prefix: &apos;(^|\\s+|\\()&apos;
    suffix: &apos;($|\\s+|\\))&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;prefix&lt;/code&gt; and &lt;code&gt;suffix&lt;/code&gt; input properties need some explanation. Originally I was just using a plain string find and replace, but I discovered that there was a problem.&lt;/p&gt;
&lt;p&gt;Consider that I could have multiple broken links to a site. eg. &lt;code&gt;blog.spencen.com/2010/09/04/word-puzzle-to-sliverlight-phonendashpart-3.aspx&lt;/code&gt; and &lt;code&gt;blog.spencen.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I replace all the instances of the first URL with &lt;code&gt;https://web.archive.org/web/20100926212957/http://blog.spencen.com/2010/09/04/word-puzzle-to-sliverlight-phonendashpart-3.aspx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The problem comes with the next find/replace in that it is now looking for &lt;code&gt;blog.spencen.com&lt;/code&gt; and that value also exists in the new snapshot URL! We potentially end up in a recursive &apos;inception&apos; mess. To avoid this, we can supply some partial regular expressions that get concatenation before and after the broken URL when we&apos;re searching. In my case those expressions mean that &lt;code&gt;blog.spencen.com&lt;/code&gt; won&apos;t be matched in the middle of the snapshot URL.&lt;/p&gt;
&lt;p&gt;I&apos;ve made these properties so the action is more general than just my use case of updating my blog posts.&lt;/p&gt;
&lt;p&gt;To actually use the action in concert with the previous actions, I&apos;m using it thus:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Replacements
  uses: flcdrg/replace-multiple-action@v1
  with:
    find: ${{ steps.wayback.outputs.replacements }}
    prefix: &apos;(^|\\s+|\\()&apos;
    suffix: &apos;($|\\s+|\\))&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key difference here is that I&apos;m making use of the output property from the Wayback Machine Query action.&lt;/p&gt;
&lt;p&gt;Our files have been updated. Next we finish off the workflow by creating a pull request with the changes.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/04/blog-fix-part3</id>
    <updated>2022-04-21T09:00:00.000+09:30</updated>
    <title>Fixing my blog (part 3) - Querying the Wayback Machine</title>
    <link href="https://david.gardiner.net.au/2022/04/blog-fix-part3" rel="alternate" type="text/html" title="Fixing my blog (part 3) - Querying the Wayback Machine"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2022-04-21T09:00:00.000+09:30</published>
    <summary type="html">Last time I&apos;d discovered I had a huge link rot problem on my blog, but I didn&apos;t fancy copy and pasting all those broken URLs into the Internet Archive&apos;s Wayback Machine to search for the equivalent archive URL.</summary>
    <content type="html">&lt;p&gt;&lt;a href=&quot;/2022/04/blog-fix-part2&quot;&gt;Last time&lt;/a&gt; I&apos;d discovered I had a huge link rot problem on my blog, but I didn&apos;t fancy copy and pasting all those broken URLs into the Internet Archive&apos;s Wayback Machine to search for the equivalent archive URL.&lt;/p&gt;
&lt;p&gt;What I need is a way to query the Wayback Machine via some kind of API.. Like maybe &lt;a href=&quot;https://archive.org/help/wayback_api.php&quot;&gt;this one?&lt;/a&gt;. The API is quite simple to use.&lt;/p&gt;
&lt;p&gt;To look up the snapshot for Nigel&apos;s old blog site, I&apos;d use the following request&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://archive.org/wayback/available?url=http://blog.spencen.com/&quot;&gt;https://archive.org/wayback/available?url=http://blog.spencen.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That returns the following JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;url&quot;: &quot;http://blog.spencen.com/&quot;,
  &quot;archived_snapshots&quot;: {
    &quot;closest&quot;: {
      &quot;status&quot;: &quot;200&quot;,
      &quot;available&quot;: true,
      &quot;url&quot;: &quot;https://web.archive.org/web/20201129195822/http://blog.spencen.com/&quot;,
      &quot;timestamp&quot;: &quot;20201129195822&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s pretty good, but we can do better. An optional &lt;code&gt;timestamp&lt;/code&gt; can be supplied in the query string (using the format &lt;code&gt;YYYYMMDDhhmmss&lt;/code&gt;), so that instead of returning the most recent available capture, instead the snapshot closest to that timestamp will be returned. This is useful for URLs where the content might have changed over time. I realised that my blog posts include the date in the filename, so if I parsed the filename I could get a timestamp with a resolution of a specific day - that would be good enough to make the snapshot URLs more accurate.&lt;/p&gt;
&lt;p&gt;So given the page that linked to Nigel&apos;s blog was named &lt;code&gt;2009-09-13-tech-ed-2009-thursday.md&lt;/code&gt;, then the query would become&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://archive.org/wayback/available?url=http://blog.spencen.com/&amp;amp;timestamp=20090913&quot;&gt;https://archive.org/wayback/available?url=http://blog.spencen.com/&amp;amp;timestamp=20090913&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And now we get this JSON result&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;url&quot;: &quot;http://blog.spencen.com/&quot;,
  &quot;archived_snapshots&quot;: {
    &quot;closest&quot;: {
      &quot;status&quot;: &quot;200&quot;,
      &quot;available&quot;: true,
      &quot;url&quot;: &quot;https://web.archive.org/web/20100315160244/http://blog.spencen.com:80/&quot;,
      &quot;timestamp&quot;: &quot;20100315160244&quot;
    }
  },
  &quot;timestamp&quot;: &quot;20090913&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browsing to that new &lt;a href=&quot;https://web.archive.org/web/20100315160244/http://blog.spencen.com:80/&quot;&gt;snapshot URL&lt;/a&gt; shows a representation of Nigel&apos;s blog as it would have been when I wrote that blog post back in 2009.&lt;/p&gt;
&lt;p&gt;So how can we automate this a bit more? By creating a new GitHub Action of course.&lt;/p&gt;
&lt;p&gt;Enter the &lt;a href=&quot;https://github.com/marketplace/actions/wayback-machine-query&quot;&gt;&lt;code&gt;Wayback Machine Query GitHub Action&lt;/code&gt;&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;If you look a the inputs for this action, it&apos;s no coincidence that the file it requires to provide the list of URLs to query the Wayback Machine with just happens to be compatible with the one generated by the &lt;a href=&quot;https://github.com/marketplace/actions/lychee-broken-link-checker&quot;&gt;Lychee Broken Link Checker&lt;/a&gt; GitHub Action we used in the previous post.&lt;/p&gt;
&lt;p&gt;My new action returns the results in two output properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;missing&lt;/code&gt; is an array of URLs that had no snapshots on the Wayback Machine. These URLs were never archived, so we&apos;ll have to deal with these differently.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replacements&lt;/code&gt; is an array of objects with the original URL and the Wayback Machine&apos;s snapshot URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The action also has an input property that allows us to provide a regular expression that can be used to parse the filename to obtain the timestamp.&lt;/p&gt;
&lt;p&gt;Here&apos;s how I&apos;m using it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: Wayback Machine Query
        uses: flcdrg/wayback-machine-query-action@v2
        id: wayback
        with:
          source-path: ./lychee/links.json
          timestamp-regex: &apos;_posts\/(\d+)\/(?&amp;lt;year&amp;gt;\d+)-(?&amp;lt;month&amp;gt;\d+)-(?&amp;lt;day&amp;gt;\d+)-&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously if your filenames or paths contain the date in a different format you&apos;d need to adjust the &lt;code&gt;timestamp-regex&lt;/code&gt; value (or if they don&apos;t contain the date then don&apos;t set that property at all).&lt;/p&gt;
&lt;p&gt;We&apos;ve now got a list of old and new URLs. In the next part we&apos;ll update the files.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/04/blog-fix-part2</id>
    <updated>2022-04-20T09:00:00.000+09:30</updated>
    <title>Fixing my blog (part 2) - Broken links</title>
    <link href="https://david.gardiner.net.au/2022/04/blog-fix-part2" rel="alternate" type="text/html" title="Fixing my blog (part 2) - Broken links"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2022-04-20T09:00:00.000+09:30</published>
    <summary type="html">My first attempt to use the Accessibility Insights Action returned some actionable results, but it also crashed with an error.</summary>
    <content type="html">&lt;p&gt;My first attempt to use the &lt;a href=&quot;https://github.com/microsoft/accessibility-insights-action&quot;&gt;Accessibility Insights Action&lt;/a&gt; returned some actionable results, but it also crashed with an error. Not a great experience, but looking closer I realised it seemed to be checking an external link that was timing out.&lt;/p&gt;
&lt;p&gt;Hmm. I&apos;ve been blogging for a few years. I guess there&apos;s the chance that the odd link might be broken? Maybe I should do something about that first, and maybe I should automate it too.&lt;/p&gt;
&lt;p&gt;A bit of searching turned up The &lt;a href=&quot;https://github.com/marketplace/actions/lychee-broken-link-checker&quot;&gt;Lychee Broken Link Checker&lt;/a&gt; GitHub Action. It turns out &apos;Lychee&apos; is the name of the &lt;a href=&quot;https://github.com/lycheeverse/lychee&quot;&gt;underlying link checker engine&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s create a GitHub workflow that uses this action and see what we get. I came up with something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Links

on:
  workflow_dispatch:
    
jobs:
  linkChecker:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Link Checker
        id: lychee
        uses: lycheeverse/lychee-action@v1.4.1
        env:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
        with:
          args: &apos;--verbose ./_posts/**/*.md --exclude-mail --scheme http https&apos;
          format: json
          output: ./lychee/links.json
          fail: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Results are reported to the workflow output, but also saved to a local file. The contents of this file look similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;total&quot;: 5262,
  &quot;successful&quot;: 4063,
  &quot;failures&quot;: 1037,
  &quot;unknown&quot;: 5,
  &quot;timeouts&quot;: 125,
  &quot;redirects&quot;: 0,
  &quot;excludes&quot;: 32,
  &quot;errors&quot;: 0,
  &quot;cached&quot;: 381,
  &quot;fail_map&quot;: {
    &quot;./_posts/2009/2009-09-13-tech-ed-2009-thursday.md&quot;: [
      {
        &quot;url&quot;: &quot;http://blog.spencen.com/&quot;,
        &quot;status&quot;: &quot;Timeout&quot;
      },
      {
        &quot;url&quot;: &quot;http://notgartner.wordpress.com/&quot;,
        &quot;status&quot;: &quot;Cached: Error (cached)&quot;
      },
      {
        &quot;url&quot;: &quot;http://adamcogan.spaces.live.com/&quot;,
        &quot;status&quot;: &quot;Failed: Network error&quot;
      }
    ],
    &quot;./_posts/2010/2010-01-22-tour-down-under-2010.md&quot;: [
      {
        &quot;url&quot;: &quot;http://www.cannondale.com/bikes/innovation/caad7/&quot;,
        &quot;status&quot;: &quot;Failed: Network error&quot;
      },
      {
        &quot;url&quot;: &quot;http://lh3.ggpht.com/&quot;,
        &quot;status&quot;: &quot;Cached: Error (cached)&quot;
      },
      {
        &quot;url&quot;: &quot;http://www.tourdownunder.com.au/race/stage-4&quot;,
        &quot;status&quot;: &quot;Failed: Network error&quot;
      }
    ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Note that there&apos;s a &lt;a href=&quot;https://github.com/lycheeverse/lychee-action/issues/100&quot;&gt;known issue that the output JSON file isn&apos;t actually valid JSON&lt;/a&gt;. Hopefully that will be fixed soon)&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;fail_map&lt;/code&gt; contains a property for each file that has failing links, and for eacho of those an array of all the links that failed (and the particular error observed). Just by looking at the links, I know that some of those websites don&apos;t even exist anymore, some might still have the website but the content has changed, and some could be transient errors. I had no idea I had so much link rot!&lt;/p&gt;
&lt;p&gt;Good memories Nigel, Mitch and Adam (the writers of those first three old sites)!&lt;/p&gt;
&lt;p&gt;Ok, let&apos;s start fixing them.&lt;/p&gt;
&lt;p&gt;But what do you replace each broken link with? Sure, some of the content might still exist at a slightly different URL, but for many the content has long gone. Except maybe it hasn&apos;t. I remembered the Internet Archive operates the &lt;a href=&quot;https://archive.org/&quot;&gt;Wayback Machine&lt;/a&gt;. So maybe I can take each broken URL, paste it into the Wayback Machine, and if there&apos;s a match, use the archive&apos;s URL instead.&lt;/p&gt;
&lt;p&gt;Except I had hundreds of broken links. Maybe I could automate this as well?&lt;/p&gt;
&lt;p&gt;Find out in part 3..&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2022/04/blog-fix-part1</id>
    <updated>2022-04-19T13:00:00.000+09:30</updated>
    <title>Fixing my blog (part 1) - Introduction</title>
    <link href="https://david.gardiner.net.au/2022/04/blog-fix-part1" rel="alternate" type="text/html" title="Fixing my blog (part 1) - Introduction"/>
    <category term="Accessibility"/>
    <category term="Blogging"/>
    <category term="GitHub Actions"/>
    <published>2022-04-19T13:00:00.000+09:30</published>
    <summary type="html">I&apos;ve been revisiting web accessibility.</summary>
    <content type="html">&lt;p&gt;I&apos;ve been revisiting web accessibility. It&apos;s something I remember first learning about accessibility many years ago at a training workshop run by &lt;a href=&quot;https://web.archive.org/web/20220527022804/https://visionaustralia.org/services/digital-access&quot;&gt;Vision Australia&lt;/a&gt; back when I worked at the University of South Australia. The web has progressed a little bit in the last 15 odd years, but the challenge of accessibility remains. More recently I had the opportunity to update my accessibility knowledge by attending a couple of presentations given by &lt;a href=&quot;https://twitter.com/LareneLg&quot;&gt;Larene Le Gassick&lt;/a&gt; (who also happens to be a fellow Microsoft MVP).&lt;/p&gt;
&lt;p&gt;I wondered how accessible my blog was. Theoretically it should be pretty good, considering it is largely text with just a few images. There shouldn&apos;t be any complicated navigation system or confusing layout. Using tools to check accessibility, and in particular compliance with a particular level of the Web Content Accessibility Guidelines (WCAG) standard will not give you the complete picture. But it can identify some deficiencies and give you confidence that particular problems have been eliminated.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://twitter.com/MrRossMullen&quot;&gt;Ross Mullen&lt;/a&gt; wrote a great article &lt;a href=&quot;https://www.canaxess.com.au/articles/gotta-catch-em-all/&quot;&gt;showing how to use the &lt;code&gt;pa11y&lt;/code&gt; GitHub Action&lt;/a&gt; as part of your continuous integration workflow to automatically scan files at build time. &lt;a href=&quot;https://github.com/pa11y/pa11y&quot;&gt;Pa11y&lt;/a&gt; is built on the &lt;a href=&quot;https://github.com/dequelabs/axe-core&quot;&gt;axe-core library&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Further research brought me to &lt;a href=&quot;https://accessibilityinsights.io/&quot;&gt;Accessibility Insights&lt;/a&gt; - Android, browser and Windows desktop accessibility tools produced by Microsoft. From here I then found that Microsoft had also made a GitHub Action (currently in development) &lt;a href=&quot;https://github.com/microsoft/accessibility-insights-action&quot;&gt;Accessibility Insights Action&lt;/a&gt;, which as I understand it, also leverages axe-core.&lt;/p&gt;
&lt;p&gt;The next few blog posts will cover my adventures working towards being able to run that action against my blog. I thought it would be simple, but it turns out I had some other issues with my blog that needed to be addressed along the way. Stay tuned!&lt;/p&gt;
</content>
  </entry>
</feed>
