<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="en">
<title>Aaron Goldenthal</title>
<subtitle>I write about software development, continuous integration/delivery, GitLab, containers, Node.js, and numerous other topics.</subtitle>
<link href="https://aarongoldenthal.com/feed/feed.xml" rel="self" />
<link href="https://aarongoldenthal.com/" />
<updated>2026-01-08T00:00:00Z</updated>
<id>https://aarongoldenthal.com/</id>
<author>
<name>Aaron Goldenthal</name>
<email>blog@aarongoldenthal.com</email>
</author>
    
    <entry>
    <title>The Year in Review 2025</title>

    <link href="https://aarongoldenthal.com/posts/the-year-in-review-2025/" />
    <updated>2026-01-08T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/the-year-in-review-2025/</id>
    <content type="html">&lt;p&gt;As usual, it&#39;s been another busy year, focused mostly of sustaining of my
existing projects and setting a new record with almost 500 software releases.
I&#39;ve continued development of new, and adoption of some existing, tools and
techniques to improve productivity and software quality. In addition, I joined
the Pa11y team where work has ramped up again resulting in several needed, major
releases.&lt;/p&gt;
&lt;h2 id=&quot;blog-posts&quot; tabindex=&quot;-1&quot;&gt;Blog posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#blog-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I didn&#39;t start this year with any specific goals for blog posts because I was so
busy with other projects that I only managed two posts. They&#39;re related to
subsequent topics in this post and are linked there.&lt;/p&gt;
&lt;h2 id=&quot;software-projects&quot; tabindex=&quot;-1&quot;&gt;Software projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#software-projects&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This year was busy with software development rather than writing, with a handful
of new items to note.&lt;/p&gt;
&lt;h3 id=&quot;i-joined-the-pa11y-team&quot; tabindex=&quot;-1&quot;&gt;I joined the Pa11y team &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#i-joined-the-pa11y-team&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Last year, with some uncertainty in support for the
&lt;a href=&quot;https://github.com/pa11y&quot;&gt;Pa11y&lt;/a&gt; projects, I had released forks of both &lt;code&gt;pa11y&lt;/code&gt;
and &lt;code&gt;pa11y-ci&lt;/code&gt; as interim fixes until the long-term plans of the team were
better understood. In 2025, I, and several others, joined the Pa11y team and
there has been a resurgence in development. This led to two major releases (and
a few minor/patch releases as well):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The release of
&lt;a href=&quot;https://github.com/pa11y/pa11y/releases/tag/9.0.0&quot;&gt;&lt;code&gt;pa11y@9.0.0&lt;/code&gt;&lt;/a&gt;, which was
the first major release in over a year.&lt;/li&gt;
&lt;li&gt;The release of
&lt;a href=&quot;https://github.com/pa11y/pa11y-ci/releases/tag/4.0.0&quot;&gt;&lt;code&gt;pa11y-ci@4.0.0&lt;/code&gt;&lt;/a&gt;. This
was the first major release upgrading from &lt;code&gt;pa11y@6.2.3&lt;/code&gt; (released in 2022) to
&lt;code&gt;pa11y@9.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Work is ongoing, so expect continued support and enhancements this year. A
significant upgrade to the &lt;code&gt;axe&lt;/code&gt; runner, improving the treatment of issues
requiring manual review (see
&lt;a href=&quot;https://github.com/pa11y/pa11y/pull/685&quot;&gt;PR #685&lt;/a&gt;), is close to release.&lt;/p&gt;
&lt;h3 id=&quot;my-open-source-software-projects&quot; tabindex=&quot;-1&quot;&gt;My open source software projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#my-open-source-software-projects&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This year was full of a lot of sustaining work on existing projects, with a few
new and noteworthy items.&lt;/p&gt;
&lt;p&gt;In 2025 I started four new projects, and between those projects and my other
existing projects shipped &lt;strong&gt;479 releases&lt;/strong&gt; (63 major, 131 minor, 276 patch),
which is a new record. Even though many of those were routine sustaining updates
(refactoring, security patches, dependency updates, etc.), it reinforces that
continuous delivery practices and extensive automation do improve productivity
and the ability to confidently release software.&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;Full list of 2025 new projects&lt;/summary&gt;
&lt;table class=&quot;first-column-350&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko&quot;&gt;Container Images / Kaniko&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A project to build container images for Chainguard&#39;s &lt;a href=&quot;https://github.com/chainguard-forks/kaniko&quot;&gt;&lt;code&gt;kaniko&lt;/code&gt;&lt;/a&gt; fork.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/psscriptanalyzer&quot;&gt;Container Images / PSScriptAnalyzer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A container image to run lint and SAST analyses on PowerShell scripts with PSScriptAnalyzer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test&quot;&gt;Tests / Container Multi-Platform Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A multi-platform container project to test CI jobs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-web-test&quot;&gt;Tests / Node Web Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Node.js project to test CI pipeline jobs for web pages/applications.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/details&gt;
&lt;details&gt;&lt;summary&gt;Full list of 2025 software releases&lt;/summary&gt;
&lt;table class=&quot;first-column-350&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Releases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc&quot;&gt;Container Images / Cloc&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;9&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.6.2&quot;&gt;1.6.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.6.1&quot;&gt;1.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.6.0&quot;&gt;1.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.5.0&quot;&gt;1.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.4.1&quot;&gt;1.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.4.0&quot;&gt;1.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.3.1&quot;&gt;1.3.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test&quot;&gt;Container Images / Go Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;24&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.9&quot;&gt;2.9.9&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.8&quot;&gt;2.9.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.7&quot;&gt;2.9.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.6&quot;&gt;2.9.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.5&quot;&gt;2.9.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.4&quot;&gt;2.9.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.3&quot;&gt;2.9.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.2&quot;&gt;2.9.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.1&quot;&gt;2.9.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.9.0&quot;&gt;2.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.8.3&quot;&gt;2.8.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.8.2&quot;&gt;2.8.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.8.1&quot;&gt;2.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.8.0&quot;&gt;2.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.7.3&quot;&gt;2.7.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.7.2&quot;&gt;2.7.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.7.1&quot;&gt;2.7.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.7.0&quot;&gt;2.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.6.1&quot;&gt;2.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.6.0&quot;&gt;2.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.8&quot;&gt;2.5.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.7&quot;&gt;2.5.7&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools&quot;&gt;Container Images / Image Tools&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;24&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/3.0.3&quot;&gt;3.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/2.2.0&quot;&gt;2.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/2.1.1&quot;&gt;2.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.8.2&quot;&gt;1.8.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.8.1&quot;&gt;1.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.8.0&quot;&gt;1.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.7.3&quot;&gt;1.7.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.7.2&quot;&gt;1.7.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.7.1&quot;&gt;1.7.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.7.0&quot;&gt;1.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.6.0&quot;&gt;1.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.5.1&quot;&gt;1.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.5.0&quot;&gt;1.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.4.1&quot;&gt;1.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.4.0&quot;&gt;1.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.3.0&quot;&gt;1.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.2.4&quot;&gt;1.2.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.2.3&quot;&gt;1.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.2.2&quot;&gt;1.2.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko&quot;&gt;Container Images / Kaniko&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;10&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.9&quot;&gt;1.0.9&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.8&quot;&gt;1.0.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.7&quot;&gt;1.0.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.6&quot;&gt;1.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.5&quot;&gt;1.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.4&quot;&gt;1.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/psscriptanalyzer&quot;&gt;Container Images / PSScriptAnalyzer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/psscriptanalyzer/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/psscriptanalyzer/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/psscriptanalyzer/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/psscriptanalyzer/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer&quot;&gt;Container Images / Puppeteer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;9&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/16.0.1&quot;&gt;16.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/16.0.0&quot;&gt;16.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/15.2.0&quot;&gt;15.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/15.1.2&quot;&gt;15.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/15.1.1&quot;&gt;15.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/15.1.0&quot;&gt;15.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/15.0.0&quot;&gt;15.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/14.2.0&quot;&gt;14.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/14.1.0&quot;&gt;14.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft&quot;&gt;Container Images / Syft&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;9&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.3.4&quot;&gt;1.3.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.3.3&quot;&gt;1.3.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.3.2&quot;&gt;1.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.3.1&quot;&gt;1.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.3.0&quot;&gt;1.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.8&quot;&gt;1.2.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.7&quot;&gt;1.2.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.6&quot;&gt;1.2.6&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale&quot;&gt;Container Images / Vale&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;29&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.8.2&quot;&gt;3.8.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.8.1&quot;&gt;3.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.8.0&quot;&gt;3.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.10&quot;&gt;3.7.10&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.9&quot;&gt;3.7.9&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.8&quot;&gt;3.7.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.7&quot;&gt;3.7.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.6&quot;&gt;3.7.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.5&quot;&gt;3.7.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.4&quot;&gt;3.7.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.3&quot;&gt;3.7.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.2&quot;&gt;3.7.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.1&quot;&gt;3.7.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.7.0&quot;&gt;3.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.6.1&quot;&gt;3.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.6.0&quot;&gt;3.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.5.5&quot;&gt;3.5.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.5.4&quot;&gt;3.5.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.5.3&quot;&gt;3.5.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.5.2&quot;&gt;3.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.5.1&quot;&gt;3.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.5.0&quot;&gt;3.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.4.1&quot;&gt;3.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.4.0&quot;&gt;3.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.6&quot;&gt;3.3.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.5&quot;&gt;3.3.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.4&quot;&gt;3.3.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.3&quot;&gt;3.3.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.2&quot;&gt;3.3.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard&quot;&gt;Node / Stylelint Config Standard&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;3&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/21.0.1&quot;&gt;21.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/21.0.0&quot;&gt;21.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/20.0.0&quot;&gt;20.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary&quot;&gt;References / GitLab Group Project Summary&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.1.3&quot;&gt;1.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.1.2&quot;&gt;1.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test&quot;&gt;Tests / Container Multi-Platform Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test&quot;&gt;Tests / Container Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;9&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.2.1&quot;&gt;1.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.1.4&quot;&gt;1.1.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.1.3&quot;&gt;1.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.1.2&quot;&gt;1.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.6&quot;&gt;1.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.5&quot;&gt;1.0.5&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test&quot;&gt;Tests / Node CJS Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/3.0.3&quot;&gt;3.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test&quot;&gt;Tests / Node ESM Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;8&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/2.0.5&quot;&gt;2.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/2.0.4&quot;&gt;2.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/2.0.3&quot;&gt;2.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/2.0.2&quot;&gt;2.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-web-test&quot;&gt;Tests / Node Web Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-web-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester&quot;&gt;Bin Tester&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/7.0.1&quot;&gt;7.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger&quot;&gt;CI Logger&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger/-/releases/8.0.1&quot;&gt;8.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files&quot;&gt;Config Files&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;15&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/13.3.1&quot;&gt;13.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/13.3.0&quot;&gt;13.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/13.2.0&quot;&gt;13.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/13.1.0&quot;&gt;13.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/13.0.0&quot;&gt;13.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/12.2.0&quot;&gt;12.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/12.1.0&quot;&gt;12.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/12.0.1&quot;&gt;12.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/12.0.0&quot;&gt;12.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.5.1&quot;&gt;11.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.5.0&quot;&gt;11.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.4.0&quot;&gt;11.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.3.0&quot;&gt;11.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.2.1&quot;&gt;11.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.2.0&quot;&gt;11.2.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq&quot;&gt;Curl jq&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;8&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/4.0.1&quot;&gt;4.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.2.3&quot;&gt;3.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.2.2&quot;&gt;3.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.2.1&quot;&gt;3.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.2.0&quot;&gt;3.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.1.2&quot;&gt;3.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.1.1&quot;&gt;3.1.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check&quot;&gt;Dependency Check&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;13&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.5.2&quot;&gt;4.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.5.1&quot;&gt;4.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.5.0&quot;&gt;4.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.4.5&quot;&gt;4.4.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.4.4&quot;&gt;4.4.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.4.3&quot;&gt;4.4.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.4.2&quot;&gt;4.4.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.4.1&quot;&gt;4.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.4.0&quot;&gt;4.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.3.0&quot;&gt;4.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.2.2&quot;&gt;4.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.2.1&quot;&gt;4.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.2.0&quot;&gt;4.2.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates&quot;&gt;Docker Sokrates&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;7&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.10&quot;&gt;3.0.10&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.9&quot;&gt;3.0.9&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.8&quot;&gt;3.0.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.7&quot;&gt;3.0.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.6&quot;&gt;3.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.5&quot;&gt;3.0.5&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard&quot;&gt;ESLint Config Standard&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;16&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/43.0.0&quot;&gt;43.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/42.0.1&quot;&gt;42.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/42.0.0&quot;&gt;42.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/41.0.0&quot;&gt;41.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/40.0.0&quot;&gt;40.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/39.0.2&quot;&gt;39.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/39.0.1&quot;&gt;39.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/39.0.0&quot;&gt;39.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/38.0.1&quot;&gt;38.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/38.0.0&quot;&gt;38.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/37.1.0&quot;&gt;37.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/37.0.0&quot;&gt;37.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/36.0.0&quot;&gt;36.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/35.0.1&quot;&gt;35.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/35.0.0&quot;&gt;35.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/34.0.0&quot;&gt;34.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env&quot;&gt;GitLab CI Env&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/11.3.0&quot;&gt;11.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/11.2.0&quot;&gt;11.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/11.1.0&quot;&gt;11.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/11.0.0&quot;&gt;11.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/10.3.0&quot;&gt;10.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/10.2.0&quot;&gt;10.2.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;GitLab CI Templates&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;32&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/49.1.0&quot;&gt;49.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/49.0.0&quot;&gt;49.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/48.1.0&quot;&gt;48.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/48.0.0&quot;&gt;48.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/47.0.0&quot;&gt;47.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/46.0.1&quot;&gt;46.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/46.0.0&quot;&gt;46.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/45.0.0&quot;&gt;45.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/44.5.0&quot;&gt;44.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/44.4.0&quot;&gt;44.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/44.3.0&quot;&gt;44.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/44.2.0&quot;&gt;44.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/44.1.0&quot;&gt;44.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/44.0.0&quot;&gt;44.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/43.1.0&quot;&gt;43.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/43.0.0&quot;&gt;43.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/42.0.0&quot;&gt;42.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/41.0.0&quot;&gt;41.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/40.0.0&quot;&gt;40.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/39.0.3&quot;&gt;39.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/39.0.2&quot;&gt;39.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/39.0.1&quot;&gt;39.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/39.0.0&quot;&gt;39.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.3.1&quot;&gt;38.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.3.0&quot;&gt;38.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.2.0&quot;&gt;38.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.1.2&quot;&gt;38.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.1.1&quot;&gt;38.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.1.0&quot;&gt;38.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/38.0.0&quot;&gt;38.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.5.0&quot;&gt;37.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.4.0&quot;&gt;37.4.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd&quot;&gt;GitLab PMD CPD&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;25&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.6&quot;&gt;2.10.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.5&quot;&gt;2.10.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.4&quot;&gt;2.10.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.3&quot;&gt;2.10.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.2&quot;&gt;2.10.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.1&quot;&gt;2.10.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.10.0&quot;&gt;2.10.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.9.0&quot;&gt;2.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.8.2&quot;&gt;2.8.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.8.1&quot;&gt;2.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.8.0&quot;&gt;2.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.7.3&quot;&gt;2.7.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.7.2&quot;&gt;2.7.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.7.1&quot;&gt;2.7.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.7.0&quot;&gt;2.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.6.0&quot;&gt;2.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.6&quot;&gt;2.5.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.5&quot;&gt;2.5.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.4&quot;&gt;2.5.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.3&quot;&gt;2.5.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.2&quot;&gt;2.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.1&quot;&gt;2.5.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci&quot;&gt;GitLab Pa11y CI&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;39&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.3.1&quot;&gt;13.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.3.0&quot;&gt;13.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.2.1&quot;&gt;13.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.2.0&quot;&gt;13.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.1.1&quot;&gt;13.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.1.0&quot;&gt;13.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.0.1&quot;&gt;13.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/13.0.0&quot;&gt;13.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.6.1&quot;&gt;12.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.6.0&quot;&gt;12.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.5.1&quot;&gt;12.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.5.0&quot;&gt;12.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.4.1&quot;&gt;12.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.4.0&quot;&gt;12.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.3.2&quot;&gt;12.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.3.1&quot;&gt;12.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.3.0&quot;&gt;12.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.2.1&quot;&gt;12.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.2.0&quot;&gt;12.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.1.1&quot;&gt;12.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.1.0&quot;&gt;12.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/12.0.0&quot;&gt;12.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.7.3&quot;&gt;11.7.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.7.2&quot;&gt;11.7.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.7.1&quot;&gt;11.7.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.7.0&quot;&gt;11.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.6.1&quot;&gt;11.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.6.0&quot;&gt;11.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.5.4&quot;&gt;11.5.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.5.3&quot;&gt;11.5.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.5.2&quot;&gt;11.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.5.1&quot;&gt;11.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.5.0&quot;&gt;11.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.4.0&quot;&gt;11.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.3.4&quot;&gt;11.3.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.3.3&quot;&gt;11.3.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.3.2&quot;&gt;11.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.3.1&quot;&gt;11.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.3.0&quot;&gt;11.3.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser&quot;&gt;GitLab Releaser&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/9.0.1&quot;&gt;9.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/9.0.0&quot;&gt;9.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.6&quot;&gt;8.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.5&quot;&gt;8.0.5&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus&quot;&gt;GitLab Semgrep Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;24&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/11.1.0&quot;&gt;11.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/11.0.0&quot;&gt;11.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.5.1&quot;&gt;10.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.5.0&quot;&gt;10.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.4.1&quot;&gt;10.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.4.0&quot;&gt;10.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.3.0&quot;&gt;10.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.2.5&quot;&gt;10.2.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.2.4&quot;&gt;10.2.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.2.3&quot;&gt;10.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.2.2&quot;&gt;10.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.2.1&quot;&gt;10.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.2.0&quot;&gt;10.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.1.0&quot;&gt;10.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.0.4&quot;&gt;10.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.0.3&quot;&gt;10.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.0.2&quot;&gt;10.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.0.1&quot;&gt;10.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/10.0.0&quot;&gt;10.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.5.0&quot;&gt;9.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.4.0&quot;&gt;9.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.3.0&quot;&gt;9.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.2.0&quot;&gt;9.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.1.4&quot;&gt;9.1.4&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy&quot;&gt;GitLab Webhook Renovate Proxy&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;13&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.12.2&quot;&gt;0.12.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.12.1&quot;&gt;0.12.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.12.0&quot;&gt;0.12.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.11.0&quot;&gt;0.11.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.10.0&quot;&gt;0.10.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.9.0&quot;&gt;0.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.8.1&quot;&gt;0.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.8.0&quot;&gt;0.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.7.0&quot;&gt;0.7.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse&quot;&gt;Lighthouse&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;55&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.2.3&quot;&gt;8.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.2.2&quot;&gt;8.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.2.1&quot;&gt;8.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.2.0&quot;&gt;8.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.1.3&quot;&gt;8.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.1.2&quot;&gt;8.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.1.1&quot;&gt;8.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.1.0&quot;&gt;8.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.0.2&quot;&gt;8.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.0.1&quot;&gt;8.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.17.1&quot;&gt;7.17.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.17.0&quot;&gt;7.17.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.16.3&quot;&gt;7.16.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.16.2&quot;&gt;7.16.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.16.1&quot;&gt;7.16.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.16.0&quot;&gt;7.16.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.15.4&quot;&gt;7.15.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.15.3&quot;&gt;7.15.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.15.2&quot;&gt;7.15.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.15.1&quot;&gt;7.15.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.15.0&quot;&gt;7.15.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.14.4&quot;&gt;7.14.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.14.3&quot;&gt;7.14.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.14.2&quot;&gt;7.14.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.14.1&quot;&gt;7.14.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.14.0&quot;&gt;7.14.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.13.1&quot;&gt;7.13.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.13.0&quot;&gt;7.13.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.12.3&quot;&gt;7.12.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.12.2&quot;&gt;7.12.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.12.1&quot;&gt;7.12.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.12.0&quot;&gt;7.12.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.11.1&quot;&gt;7.11.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.11.0&quot;&gt;7.11.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.10.3&quot;&gt;7.10.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.10.2&quot;&gt;7.10.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.10.1&quot;&gt;7.10.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.10.0&quot;&gt;7.10.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.9.0&quot;&gt;7.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.8.2&quot;&gt;7.8.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.8.1&quot;&gt;7.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.8.0&quot;&gt;7.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.7.0&quot;&gt;7.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.6.2&quot;&gt;7.6.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.6.1&quot;&gt;7.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.6.0&quot;&gt;7.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.5.0&quot;&gt;7.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.4.2&quot;&gt;7.4.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.4.1&quot;&gt;7.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.4.0&quot;&gt;7.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.3.2&quot;&gt;7.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.3.1&quot;&gt;7.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.3.0&quot;&gt;7.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.2.3&quot;&gt;7.2.3&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary&quot;&gt;Pa11y CI CLI Summary Reporter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary/-/releases/5.0.1&quot;&gt;5.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html&quot;&gt;Pa11y CI HTML Reporter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/8.0.1&quot;&gt;8.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner&quot;&gt;Pa11y CI Reporter Runner&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/6.0.2&quot;&gt;6.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/6.0.1&quot;&gt;6.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/5.0.5&quot;&gt;5.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/5.0.4&quot;&gt;5.0.4&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus&quot;&gt;Pa11y HTML Reporter Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/5.0.1&quot;&gt;5.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean&quot;&gt;Pagean&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;41&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.3.1&quot;&gt;15.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.3.0&quot;&gt;15.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.2.1&quot;&gt;15.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.2.0&quot;&gt;15.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.1.0&quot;&gt;15.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.0.1&quot;&gt;15.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/15.0.0&quot;&gt;15.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.10.1&quot;&gt;14.10.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.10.0&quot;&gt;14.10.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.9.1&quot;&gt;14.9.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.9.0&quot;&gt;14.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.8.0&quot;&gt;14.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.7.1&quot;&gt;14.7.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.7.0&quot;&gt;14.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.6.0&quot;&gt;14.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.5.1&quot;&gt;14.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.5.0&quot;&gt;14.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.4.1&quot;&gt;14.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.4.0&quot;&gt;14.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.3.0&quot;&gt;14.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.2.3&quot;&gt;14.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.2.2&quot;&gt;14.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.2.1&quot;&gt;14.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.2.0&quot;&gt;14.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.1.1&quot;&gt;14.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.1.0&quot;&gt;14.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.0.2&quot;&gt;14.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.0.1&quot;&gt;14.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/14.0.0&quot;&gt;14.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.6.0&quot;&gt;13.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.5.3&quot;&gt;13.5.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.5.2&quot;&gt;13.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.5.1&quot;&gt;13.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.5.0&quot;&gt;13.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.4.0&quot;&gt;13.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.3.2&quot;&gt;13.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.3.1&quot;&gt;13.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.3.0&quot;&gt;13.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.2.1&quot;&gt;13.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.2.0&quot;&gt;13.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.1.1&quot;&gt;13.1.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog&quot;&gt;Releaselog&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/7.0.1&quot;&gt;7.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/6.0.3&quot;&gt;6.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/6.0.2&quot;&gt;6.0.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config&quot;&gt;Renovate Config&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;7&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/4.1.1&quot;&gt;4.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/4.1.0&quot;&gt;4.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/4.0.1&quot;&gt;4.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu&quot;&gt;Vale Rules for GitLab CI Utils&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.7&quot;&gt;1.0.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.6&quot;&gt;1.0.6&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/details&gt;
&lt;p&gt;The significant projects and events for the year are summarized in the
subsequent sections (ordered predominantly by significance).&lt;/p&gt;
&lt;h4 id=&quot;npm-supply-chain-security&quot; tabindex=&quot;-1&quot;&gt;NPM supply chain security &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#npm-supply-chain-security&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;This year saw numerous, large-scale NPM supply chain attacks. Given the ease of
publishing, &lt;em&gt;huge&lt;/em&gt; catalog of packages, casual treatment of transitive
dependencies, and large developer base, it&#39;s an attractive target. I was not
directly impacted, but it did open the community&#39;s eyes to the risks and force
mitigating actions.&lt;/p&gt;
&lt;p&gt;In July, NPM introduced Trusted Publishing
(&lt;a href=&quot;https://docs.npmjs.com/trusted-publishers&quot;&gt;docs&lt;/a&gt;,
&lt;a href=&quot;https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/&quot;&gt;blog post&lt;/a&gt;)
to significantly improve the integrity of the publishing process. This
eliminates the need for publishing tokens that could be compromised (which we
saw this year) and focuses on publishing from CI/CD workflows on trusted
platforms. Individual NPM packages must register a specific GitLab or GitHub
project as the source, and the CI pipeline or workflow file with the publishing
script, then that CI job from that project on that platform can use the platform
OIDC to authenticate to NPM and publish projects. This, along with the
&lt;a href=&quot;https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/&quot;&gt;elimination of long-lived publishing tokens&lt;/a&gt;,
are significant improvements to the integrity of NPM publishing, although
there&#39;s still more improvements that could be made. For my part, all of my
published NPM packages have moved to trusted publishing.&lt;/p&gt;
&lt;p&gt;In addition, it was observed that the large user base, as well as an
ever-growing list of security companies, do fairly quickly identify compromised
packages. Therefore, allowing some delay after publishing, but before adoption,
has been shown to allow community identification of many compromised packages
and take mitigating actions. With that, tools like Renovate have adopted a
default &lt;code&gt;minimumReleaseAge&lt;/code&gt; setting for NPM packages requiring several days for
this to occur before updating, and I&#39;ve adopted that philosophy in my
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config&quot;&gt;Renovate config presets&lt;/a&gt;,
which are used in all of my projects.&lt;/p&gt;
&lt;h4 id=&quot;type-stripping-is-stable-in-node-24&quot; tabindex=&quot;-1&quot;&gt;Type stripping is stable in Node 24 &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#type-stripping-is-stable-in-node-24&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I don&#39;t think this got nearly as much discussion as it deserved, but with the
release of Node 24.12.0
(&lt;a href=&quot;https://nodejs.org/en/blog/release/v24.12.0&quot;&gt;release notes&lt;/a&gt;), the ability for
Node to
&lt;a href=&quot;https://nodejs.org/docs/latest-v24.x/api/typescript.html#typescript-features&quot;&gt;natively execute TypeScript files&lt;/a&gt;
through type-stripping, without a build or transpilation step, is now &lt;strong&gt;stable&lt;/strong&gt;
in the Active LTS release (as well as Node 25, the current release). As a
package author, I&#39;ve published types with libraries for years, but have been
resistant to fully adopting TypeScript in those packages, instead leveraging
JSDoc types, because of the added overhead and complexity of a build step. The
Node implementation does impose some limitations on the use of a few TypeScript
features, although one (like me) could argue that these features go beyond pure
typing and create new JavaScript when transpiled, so this &lt;em&gt;limitation&lt;/em&gt; is not
only tolerable, it&#39;s desirable. While it will be some time before this is stable
in all supported Node versions (April 2027 by my check of the
&lt;a href=&quot;https://github.com/nodejs/Release&quot;&gt;Node release schedule&lt;/a&gt;, which is only 15
months away), it&#39;s a significant step forward for the ecosystem.&lt;/p&gt;
&lt;p&gt;With that, late this year I converted one CLI tool to TypeScript (see
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/2.0.0&quot;&gt;this release&lt;/a&gt;),
and plan to continue as appropriate in 2026. As with implementing any
significant new capability, I starting with setting up robust automated tooling,
which in this case was an updated ESLint configuration with TypeScript parsing
and rules. Given my opinionated stance on linting, it did take some time to get
through all of the &lt;code&gt;typescript-eslint&lt;/code&gt; rules and setup configurations that
properly supported JavaScript (CJS and ESM) and TypeScript files, but the end
result is a highly opinionated configuration that I&#39;m happy with, which was
published as my last release of the year (see
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/43.0.0&quot;&gt;this release&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;chainguard&#39;s-kaniko-container-image&quot; tabindex=&quot;-1&quot;&gt;Chainguard&#39;s Kaniko container image &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#chainguard&#39;s-kaniko-container-image&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;In mid-2025, after virtually no support for a year, Google officially archived
Kaniko, a tool introduced in 2018 to allow building container images in
Kubernetes or other containerized environments since it did not rely on the
Docker daemon. Shortly afterwards, the Chainguard team, including several of the
original Kaniko authors, announced that they had created and were maintaining a
fork as a replacement.&lt;/p&gt;
&lt;p&gt;While they are maintaining that fork, and have had six releases this year, they
are only maintaining the &lt;em&gt;code&lt;/em&gt;, not providing any binary artifacts, including a
container image. So, I created
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko&quot;&gt;this Kaniko project&lt;/a&gt;,
which is simply a CI pipeline and infrastructure to build, test, and deploy a
Kaniko container image (built from Kaniko&#39;s Dockerfile) to GitLab&#39;s container
registry (it doesn&#39;t contain the Kaniko code, that&#39;s pulled from the Chainguard
fork repository in CI). You can read the full story in
&lt;a href=&quot;https://aarongoldenthal.com/posts/google-kaniko-is-dead-long-live-chainguard-kaniko/&quot;&gt;this blog post&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;renovate-responsiveness-to-gitlab-repository-actions&quot; tabindex=&quot;-1&quot;&gt;Renovate responsiveness to GitLab repository actions &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#renovate-responsiveness-to-gitlab-repository-actions&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;My last project of 2024 was the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy&quot;&gt;GitLab Webhook Renovate Proxy&lt;/a&gt;,
which is a Cloudflare Worker that intercepts GitLab merge request and issue
webhooks and forwards those that should trigger a
&lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt; pipeline to re-analyze a project. This
year is saw extensive use, and reinforced how effective it is in improving the
responsiveness of Renovate running in GitLab CI (beyond simply running scheduled
Renovate pipelines). A summary with additional detail is available in
&lt;a href=&quot;https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/&quot;&gt;this blog post&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;mutation-testing&quot; tabindex=&quot;-1&quot;&gt;Mutation testing &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#mutation-testing&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Last, but certainly not least, I have begun experimentation with mutation
testing, incorporating &lt;a href=&quot;https://stryker-mutator.io/&quot;&gt;StrykerJS&lt;/a&gt; into several of
my projects to evaluate its effectiveness in improving test coverage and
quality. From their website:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Mutation testing introduces changes to your code, then runs your unit tests
against the changed code. It is expected that your unit tests will now fail.
If they don&#39;t fail, it might indicate your tests do not sufficiently cover the
code.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bugs, or mutants, are automatically inserted into your production code. Your
tests are run for each mutant. If your tests fail then the mutant is killed.
If your tests passed, the mutant survived. The higher the percentage of
mutants killed, the more effective your tests are.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So far it has proved easy to set up and effective at finding gaps in test
coverage, although it can be slow given the numerous variations of the test
suite that are run.&lt;/p&gt;
&lt;h4 id=&quot;gitlab-ci-template-tests&quot; tabindex=&quot;-1&quot;&gt;GitLab CI template tests &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#gitlab-ci-template-tests&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Last year, the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;GitLab CI Templates&lt;/a&gt;
project&#39;s CI pipeline was updated to trigger multi-project downstream pipelines
to allow enhanced testing of templates in the appropriate project types. That
was augmented this year with several new test projects to cover additional
scenarios including
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-multi-platform-test&quot;&gt;multi-platform container images&lt;/a&gt;
(which had new jobs and collections added in 2025) and
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-web-test&quot;&gt;web application testing&lt;/a&gt;
(which was previously untested). While there are still a few jobs that are not
currently tested, this testing now covers the vast majority of the GitLab CI
templates project.&lt;/p&gt;
&lt;h3 id=&quot;contributions-to-other-open-source-projects&quot; tabindex=&quot;-1&quot;&gt;Contributions to other open source projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#contributions-to-other-open-source-projects&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I also made contributions to a few other open source projects this year,
including &lt;a href=&quot;https://github.com/webpro-nl/knip/pull/1112&quot;&gt;&lt;code&gt;knip&lt;/code&gt;&lt;/a&gt; and Chainguard&#39;s
fork of &lt;a href=&quot;https://github.com/chainguard-forks/kaniko/pull/38&quot;&gt;&lt;code&gt;kaniko&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;looking-forward-to-2026&quot; tabindex=&quot;-1&quot;&gt;Looking forward to 2026 &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2025/#looking-forward-to-2026&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Some of the things I&#39;m looking forward to or will hopefully be resolved in 2026
include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Node.js Release Working Group team has been iterating on
&lt;a href=&quot;https://github.com/nodejs/Release/issues/1113&quot;&gt;proposed changes to the Node.js release and support philosophy&lt;/a&gt;.
It&#39;s not set in stone yet, but looks like it may take effect in 2026.&lt;/li&gt;
&lt;li&gt;This year included some evaluation of container image hardening techniques,
which will continue in 2026. This includes migrating to hardened container
base images (for example
&lt;a href=&quot;https://docs.docker.com/dhi/&quot;&gt;Docker Hardened Images&lt;/a&gt;,
&lt;a href=&quot;https://images.chainguard.dev/directory?category=free&quot;&gt;Chainguard Images&lt;/a&gt;,
&lt;a href=&quot;https://github.com/GoogleContainerTools/distroless&quot;&gt;Google Distroless Images&lt;/a&gt;,
or others) and container image and software bill-of-materials (SBOM) signing
(including container registry support, which is evolving, although very
slowly).&lt;/li&gt;
&lt;li&gt;Continuing to migrate projects to leverage mutation testing and investigate
options for improving CI pipeline performance.&lt;/li&gt;
&lt;li&gt;The future of Kaniko, as it was this year, is a little uncertain. In addition
to the Chainguard fork, a
&lt;a href=&quot;https://github.com/osscontainertools/kaniko&quot;&gt;community fork&lt;/a&gt; has been getting
some attention. It doesn&#39;t have corporate backing like the Chainguard fork,
but it has been more actively developed this year, including security/bug
fixes as well as adding missing capabilities (the &lt;code&gt;--annotation&lt;/code&gt; flag, for
example). I&#39;ll be watching both to see how they evolve this year.&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
    
    <entry>
    <title>Google Kaniko is dead, long live Chainguard Kaniko</title>

    <link href="https://aarongoldenthal.com/posts/google-kaniko-is-dead-long-live-chainguard-kaniko/" />
    <updated>2025-07-22T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/google-kaniko-is-dead-long-live-chainguard-kaniko/</id>
    <content type="html">&lt;p&gt;Google has officially archived Kaniko, a tool for building container images in
Kubernetes or other containerized environments. Chainguard has taken over a fork
of the project, however Chainguard only provides source code, not container
images, except as part of their paid offerings. To address this gap, the GitLab
CI Utils Kaniko project builds and distributes open source Kaniko container
images focused on running in GitLab CI pipelines.&lt;/p&gt;
&lt;h2 id=&quot;google-kaniko-is-dead...&quot; tabindex=&quot;-1&quot;&gt;Google Kaniko is dead… &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/google-kaniko-is-dead-long-live-chainguard-kaniko/#google-kaniko-is-dead...&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Google
&lt;a href=&quot;https://cloud.google.com/blog/products/containers-Kubernetes/introducing-kaniko-build-container-images-in-Kubernetes-and-google-container-builder-even-without-root-access&quot;&gt;introduced&lt;/a&gt;
&lt;a href=&quot;https://github.com/GoogleContainerTools/kaniko&quot;&gt;Kaniko&lt;/a&gt; in 2018 as a tool to
build container images in Kubernetes or other containerized environments since
it did not rely on the Docker daemon. It was the tool of choice in these
environments for several years. Then, in mid-2024, commits to the repository and
Kaniko releases stopped. People then began to
&lt;a href=&quot;https://github.com/GoogleContainerTools/kaniko/issues/3348&quot;&gt;question its status&lt;/a&gt;.
There was some hope when Google released a
&lt;a href=&quot;https://github.com/GoogleContainerTools/kaniko/releases/tag/v1.24.0&quot;&gt;new version&lt;/a&gt;
in May 23, 2025. However, this hope was short-lived, and on June 3, 2025 Google
officially archived the project with the message &amp;quot;this project is archived and
no longer developed or maintained.&amp;quot;&lt;/p&gt;
&lt;h2 id=&quot;...long-live-chainguard-kaniko&quot; tabindex=&quot;-1&quot;&gt;…long live Chainguard Kaniko &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/google-kaniko-is-dead-long-live-chainguard-kaniko/#...long-live-chainguard-kaniko&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Within a day of the Google announcement, the Chainguard team created a
&lt;a href=&quot;https://github.com/chainguard-dev/kaniko&quot;&gt;fork&lt;/a&gt;, followed by a
&lt;a href=&quot;https://www.chainguard.dev/unchained/fork-yeah-were-bringing-kaniko-back&quot;&gt;formal announcement&lt;/a&gt;
with their plans to actively maintain this fork as a replacement unless another
community fork emerged. Even better, two of the original creators, Priya Wadhwa
and Dan Lorenc, work at Chainguard. Within a week they had their
&lt;a href=&quot;https://github.com/chainguard-dev/kaniko/releases/tag/v1.25.0&quot;&gt;first release&lt;/a&gt;,
and it appears the project is in good hands.&lt;/p&gt;
&lt;p&gt;There was one wrinkle, however. The
&lt;a href=&quot;https://github.com/chainguard-dev/kaniko?tab=readme-ov-file#releases&quot;&gt;release plan&lt;/a&gt;
for the project includes only source code. No binary artifacts, including
container images, are provided. Chainguard does offer a different, hardened
Kaniko container images as part of their paid offerings, but no build of the new
fork. That&#39;s not a stab at Chainguard - maintenance of the project itself is
hugely beneficial to the community, done without obligation, and a non-hardened
image would be off-brand given their product line.&lt;/p&gt;
&lt;h2 id=&quot;so-here&#39;s-a-container-image&quot; tabindex=&quot;-1&quot;&gt;So, here&#39;s a container image &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/google-kaniko-is-dead-long-live-chainguard-kaniko/#so-here&#39;s-a-container-image&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;That lead to the creation of the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/kaniko&quot;&gt;GitLab CI Utils Kaniko&lt;/a&gt;
project. This project is not a fork a Kaniko, it&#39;s simply a project to build and
distribute an open source Kaniko container image built from the
&lt;a href=&quot;https://github.com/chainguard-dev/kaniko&quot;&gt;Chainguard Kaniko&lt;/a&gt; repository. It
simply clones the repository and then builds the Dockerfile.&lt;/p&gt;
&lt;p&gt;Since the focus is on use in GitLab CI, only the &lt;code&gt;debug&lt;/code&gt; variant of the image is
built. This includes a shell, which is required to use Kaniko in GitLab CI
pipelines. All images are multi-platform, supporting both &lt;code&gt;linux/arm64&lt;/code&gt; and
&lt;code&gt;linux/amd64&lt;/code&gt; (since gitlab.com has shared runners with those architectures to
test the images). There is a &lt;code&gt;debug&lt;/code&gt; tag, equivalent to what is typically the
&lt;code&gt;latest&lt;/code&gt; tag, but sticking with the previous Kaniko tagging convention, as well
as &lt;code&gt;vX.Y.Z-debug&lt;/code&gt; tags corresponding to each Chainguard Kaniko release.&lt;/p&gt;
&lt;p&gt;The project also contains usage details, as well as links to the original GitLab
documentation for using Kaniko in GitLab CI, which had been removed prematurely
(IMHO).&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Configure Renovate to Respond to GitLab Repository Actions</title>

    <link href="https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/" />
    <updated>2025-06-28T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/</id>
    <content type="html">&lt;p&gt;The typical GitLab use case is to use the Renovate runner project templates, or
some variant, to run Renovate in a GitLab CI job on a schedule. If you&#39;re
looking for an option to provide a more responsive experience without the
overhead of self-hosting Renovate, this solution uses Cloudflare Workers, GitLab
webhooks, and triggered GitLab pipelines to configure Renovate to respond to
repository activity.&lt;/p&gt;
&lt;h2 id=&quot;renovate-setup-on-gitlab&quot; tabindex=&quot;-1&quot;&gt;Renovate setup on GitLab &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/#renovate-setup-on-gitlab&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The recommended GitLab setup, as detailed in the Renovate documentation, is to
use the &lt;a href=&quot;https://gitlab.com/renovate-bot/renovate-runner&quot;&gt;Renovate runner&lt;/a&gt;
project templates, or some variant, to run Renovate in a GitLab CI job on a
schedule. This is a good starting point, but lacks much of the experience that
you get with the GitHub app or
&lt;a href=&quot;https://github.com/mend/renovate-ce-ee/blob/main/docs/overview.md&quot;&gt;Renovate self-hosted&lt;/a&gt;,
as can be seen in this Renovate
&lt;a href=&quot;https://www.mend.io/renovate/&quot;&gt;version comparison&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&#39;re looking for an option to improve the experience, without the overhead
of self-hosting, this solution using the GitLab Webhook Renovate Proxy handles
the &amp;quot;responds to repo activity&amp;quot; case using
&lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt;, GitLab webhooks, and
triggered GitLab pipelines. Cloudflare Workers provide a lightweight, easy to
deploy solution that in many cases can be used across large GitLab groups on
their free tier (although you may want a paid tier to provide support or other
services). Cloudflare Worker also have very short cold start times, so they can
be scaled to zero with no activity and still provide a quick response. According
to the last build, this Worker&#39;s startup time is approximately 12 ms.&lt;/p&gt;
&lt;h2 id=&quot;the-gitlab-webhook-renovate-proxy-solution&quot; tabindex=&quot;-1&quot;&gt;The GitLab Webhook Renovate Proxy solution &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/#the-gitlab-webhook-renovate-proxy-solution&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The GitLab Webhook Renovate Proxy is part of a bigger setup to make Renovate
updates with GitLab CI respond to repository activity. It includes the following
components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitLab Project&lt;/strong&gt;: Any GitLab project that uses Renovate for dependency
updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitLab Webhook Renovate Proxy&lt;/strong&gt;: A Cloudflare Worker that filters webhooks
so that only authenticated, applicable events trigger a Renovate pipelines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Renovate Runner&lt;/strong&gt;: The project that runs Renovate pipelines using the
&lt;a href=&quot;https://gitlab.com/renovate-bot/renovate-runner&quot;&gt;Mend &lt;code&gt;renovate-runner&lt;/code&gt;&lt;/a&gt;
project (or a fork, mirror, etc).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The integrated system works as shown below. Webhooks are configured in GitLab to
fire for appropriate events (for example, merge request and issue events). These
webhooks are sent to the GitLab Webhook Renovate Proxy, which checks that the
webhook is for an event that should run a Renovate pipeline, and if so sends a
pipeline trigger request to the GitLab Renovate Runner project specifying that
project as the target.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;sequenceDiagram
  participant G as GitLab Project
  participant P as GitLab Webhook Renovate Proxy
  participant R as Renovate Runner

  G-&amp;#x3E;&amp;#x3E;P: Webhook (Merge request open)
  G-&amp;#x3E;&amp;#x3E;P: Webhook (Merge request merge)
  P-&amp;#x3E;&amp;#x3E;R: Pipeline trigger request
  R-&amp;#x3E;&amp;#x3E;R: Run pipeline for project
  G-&amp;#x3E;&amp;#x3E;P: Webhook (Dependency Dashboard rebase branch)
  P-&amp;#x3E;&amp;#x3E;R: Pipeline trigger request
  R-&amp;#x3E;&amp;#x3E;R: Run pipeline for project
&lt;/pre&gt;
&lt;p&gt;The GitLab Webhook Renovate Proxy triggers a Renovate pipeline for the following
events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Merge request merge&lt;/li&gt;
&lt;li&gt;Merge request &amp;quot;Rebase this MR&amp;quot; checkbox&lt;/li&gt;
&lt;li&gt;All Dependency Dashboard checkboxes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The proxy primarily uses the same information that Renovate itself uses to
respond to these events - comments in the description for Renovate created merge
requests and Dependency Dashboard issues. The GitLab Webhook Renovate Proxy does
not constrain what Renovate updates, it simply determines if a Renovate pipeline
should be triggered based on the webhook event. This provides a quick response
to these events rather than having to wait for the next scheduled Renovate
pipeline.&lt;/p&gt;
&lt;p&gt;While the GitLab Webhook Renovate Proxy is written as Cloudflare Worker, it
could easily be adapted to run in other similar environments (for example,
&lt;a href=&quot;https://knative.dev/docs/&quot;&gt;Knative&lt;/a&gt; in a self-hosted environment).&lt;/p&gt;
&lt;h2 id=&quot;system-setup&quot; tabindex=&quot;-1&quot;&gt;System setup &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/#system-setup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;See the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/blob/main/docs/setup.md&quot;&gt;Setup Guide&lt;/a&gt;
for detailed instructions on end-to-end setup of the GitLab Webhook Renovate
Proxy. While it may seem complex, there are really just a lot of small steps,
and the instructions are deliberately explicit for anyone unfamiliar with
aspects of the process and to ensure that secrets (or potential secrets, for
example project IDs for private project) are appropriately protected. For any
problems, see the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/blob/main/docs/troubleshooting.md&quot;&gt;troubleshooting guide&lt;/a&gt;,
or
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/issues/&quot;&gt;open an issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Note that like any service exposed to the Internet, the GitLab Webhook Renovate
Proxy will likely receive requests scanning for known vulnerabilities (they&#39;ll
be obvious in the logs). The Worker is designed with an authentication mechanism
and rejects these requests, but the system alone is not designed to protect
against any large scale denial-of-service attacks. While this has not been seen,
if required Cloudflare has options for
&lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/&quot;&gt;rate limiting&lt;/a&gt;
and other protections.&lt;/p&gt;
&lt;h2 id=&quot;results&quot; tabindex=&quot;-1&quot;&gt;Results &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/configure-renovate-to-respond-to-gitlab-repository-actions/#results&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This system has been in use with multiple gitlab.com groups for about 6 months.
The groups have 50 - 75 projects with updates managed by Renovate. In most cases
almost all project dependencies are as strictly versioned as practical, so there
are over 800 total Renovate-managed dependencies across the groups. Most of the
projects are also configured to rebase Renovate updates when behind the base
branch to best align with the GitLab fast forward merge strategy that the
projects primarily use. So, there are a lot of Renovate pipelines across the
groups. Renovate is run on the default GitLab hosted runner (the &lt;code&gt;small&lt;/code&gt; Linux
x86-64 Kubernetes runner) as performance has been observed to be predominantly
network bound, so a more capable runner adds limited benefit for the cost.&lt;/p&gt;
&lt;p&gt;In general, the Renovate pipelines triggered by the GitLab Webhook Renovate
Proxy run in 1 - 2 minutes total. Almost one minute is the image download
itself. Given the various ecosystems that Renovate supports there is a lot in
the image and it is fairly large (~400 MB compressed, ~1.3 GB uncompressed), and
with frequent Renovate updates (many times per day) there is minimal caching
(although the GitLab hosted runners are configured to use GCP&#39;s pull-through
image cache, so it is used when applicable).&lt;/p&gt;
&lt;p&gt;Overall, that puts the responsiveness of Renovate similar to the GitHub app for
responding to repository changes (my best comparison point having never
self-hosted Renovate Community/Enterprise). It&#39;s a huge usability improvements
for cases where you&#39;re trying to process multiple Renovate updates against a
single repository. While it does not have all of the optimizations that the
GitHub app or Renovate self-hosted do, it&#39;s a good compromise for GitLab hosted
projects that do not want the overhead of the self-hosted Renovate solution.&lt;/p&gt;
&lt;script type=&quot;module&quot; async=&quot;&quot;&gt;import mermaid from &quot;https://unpkg.com/mermaid@11.11.0/dist/mermaid.esm.min.mjs&quot;;document.addEventListener(&#39;DOMContentLoaded&#39;, mermaid.initialize({&quot;loadOnSave&quot;:true}));&lt;/script&gt;
</content>
    </entry>
    
    <entry>
    <title>The Year in Review 2024</title>

    <link href="https://aarongoldenthal.com/posts/the-year-in-review-2024/" />
    <updated>2024-12-31T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/the-year-in-review-2024/</id>
    <content type="html">&lt;p&gt;It&#39;s been another busy year - I wrote a handful of blog posts on some
long-researched automation topics, continued development of many old projects,
started over a dozen new projects including my first Cloudflare Worker, and
shipped over &lt;strong&gt;450&lt;/strong&gt; software releases. This post reviews it all.&lt;/p&gt;
&lt;h2 id=&quot;blog-posts&quot; tabindex=&quot;-1&quot;&gt;Blog posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2024/#blog-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This was year two of the return of this blog. As with year one, I had lofty
goals for how much writing I would do that did not quite work out. This year I
did, however, meet my primary goal - getting several big posts out on
alternative automation and analysis techniques, which had significant background
research and testing, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/&quot;&gt;GitLab CI Pipeline for Go Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/&quot;&gt;GitLab Code Quality Duplication Analysis With PMD CPD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Notifications for New Eleventy Posts in GitLab
(&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/&quot;&gt;Part 1&lt;/a&gt;,
&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/&quot;&gt;Part 2&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/&quot;&gt;A Better GitLab Code Quality - Part 1 - Goodbye CodeClimate&lt;/a&gt;
(Part 2 really is coming)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;software-projects&quot; tabindex=&quot;-1&quot;&gt;Software projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2024/#software-projects&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most of the 2024 involved development work on a variety of project, including my
own open source projects, contributions to other open source projects, and
starting a new fork of &lt;code&gt;pa11y&lt;/code&gt; and &lt;code&gt;pa11y-ci&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-uncertain-future-of-pa11y-and-pa11y-ci&quot; tabindex=&quot;-1&quot;&gt;The uncertain future of Pa11y and Pa11y CI &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2024/#the-uncertain-future-of-pa11y-and-pa11y-ci&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I was anticipating the release of
&lt;a href=&quot;https://github.com/pa11y/pa11y&quot;&gt;&lt;code&gt;pa11y@7.0.0&lt;/code&gt;&lt;/a&gt; and
&lt;a href=&quot;https://github.com/pa11y/pa11y-ci&quot;&gt;&lt;code&gt;pa11y-ci@4.0.0&lt;/code&gt;&lt;/a&gt; in 2024. A lot of work had
gone into various updates, primarily focused on updating from deprecated to
supported dependencies, and those updates were ready to release. This year
started with a resurgence of maintainers&#39; support for the &lt;code&gt;pa11y&lt;/code&gt; projects,
including the release of &lt;code&gt;pa11y@7.0.0&lt;/code&gt; and &lt;code&gt;pa11y@8.0.0&lt;/code&gt;. Unfortunately, after
that things fizzled out again, and there was never an official &lt;code&gt;pa11y-ci@4.0.0&lt;/code&gt;.
That&#39;s not a criticism of the &lt;code&gt;pa11y&lt;/code&gt; team - priorities can always change on any
open source project, and it&#39;s only because of the work of the core team that the
project is so useful that others want to contribute and want it to live on.&lt;/p&gt;
&lt;p&gt;My focus, however, is &lt;code&gt;pa11y-ci&lt;/code&gt;, so the lack of an official &lt;code&gt;pa11y-ci@4.0.0&lt;/code&gt;
release with a supported version of &lt;code&gt;puppeteer&lt;/code&gt; finally crossed my pain
threshold. So, in September I forked &lt;code&gt;pa11y&lt;/code&gt; into
&lt;a href=&quot;https://www.npmjs.com/package/@aarongoldenthal/pa11y&quot;&gt;&lt;code&gt;@aarongoldenthal/pa11y&lt;/code&gt;&lt;/a&gt;
and &lt;code&gt;pa11y-ci&lt;/code&gt; into
&lt;a href=&quot;https://www.npmjs.com/package/@aarongoldenthal/pa11y-ci&quot;&gt;&lt;code&gt;@aarongoldenthal/pa11y-ci&lt;/code&gt;&lt;/a&gt;.
These forks have been focused on security patches as a short-term fix until the
future of &lt;code&gt;pa11y&lt;/code&gt; and &lt;code&gt;pa11y-ci&lt;/code&gt; was better understood. This led to 14 releases
of &lt;code&gt;@aarongoldenthal/pa11y&lt;/code&gt;, now up to v8.4.4, and 13 releases
&lt;code&gt;@aarongoldenthal/pa11y-ci&lt;/code&gt;, starting with a new major release and now up to
v4.3.4. In addition, the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci&quot;&gt;&lt;code&gt;gitlab-pa11y-ci&lt;/code&gt;&lt;/a&gt;
container image was updated to use these forks.&lt;/p&gt;
&lt;p&gt;Even with these forks, it has become clear that the backlog of unresolved
vulnerabilities, bug fixes, and usability enhancements is not only
ever-increasing, but they also require more significant changes that really
deserve a dedicated fork, maybe with a new name and new branding, but definitely
with sufficient resources and community support. That saga needs to be resolved
on way or another in 2025.&lt;/p&gt;
&lt;h3 id=&quot;my-open-source-software-projects&quot; tabindex=&quot;-1&quot;&gt;My open source software projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2024/#my-open-source-software-projects&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I continued to maintain a long list of open source projects. In 2024 I started
11 new projects, and between those projects and my other existing projects I
shipped &lt;strong&gt;426 releases&lt;/strong&gt; (72 major, 118 minor, 229 patch, 7 zero), and that&#39;s
excluding the 27 releases from the &lt;code&gt;pa11y&lt;/code&gt;/&lt;code&gt;pa11y-ci&lt;/code&gt; forks noted previously.
Even though many of those were security patches or dependency updates, there was
also extensive new development, and some new projects. Given those 453 releases
were in my free time should reinforce to people that continuous delivery
practices and extensive automation are a worthwhile investment and really do
improve productivity and the ability to confidently release software.&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;Full list of 2024 new projects&lt;/summary&gt;
&lt;table class=&quot;first-column-350&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc&quot;&gt;Container Images / Cloc&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;An Alpine-based container image to run &lt;a href=&quot;https://github.com/AlDanial/cloc&quot;&gt;cloc&lt;/a&gt; to count lines of code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools&quot;&gt;Container Images / Image Tools&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A collection of tools and scripts for working with container images.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer&quot;&gt;Container Images / Puppeteer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Docker Puppeteer is a container image with the recommended configuration for using Puppeteer to drive a headless Chrome browser.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale&quot;&gt;Container Images / Vale&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A container image to run &lt;a href=&quot;https://vale.sh/&quot;&gt;Vale&lt;/a&gt; to lint prose, with pre-installed rule packages and a custom GitLab Code Quality report.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test&quot;&gt;Tests / Container Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A container project to test CI jobs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/go-test&quot;&gt;Tests / Go Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Go project to test CI jobs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test&quot;&gt;Tests / Node CJS Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Node.js CJS project to test CI jobs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test&quot;&gt;Tests / Node ESM Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Node.js ESM project to test CI jobs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy&quot;&gt;GitLab Webhook Renovate Proxy&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Cloudflare Worker to intercept GitLab webhooks and only forward events that should trigger execution of a Renovate pipeline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config&quot;&gt;Renovate Config&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A collection of reusable Renovate &lt;a href=&quot;https://docs.renovatebot.com/config-presets/&quot;&gt;config presets&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu&quot;&gt;Vale Rules for GitLab CI Utils&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Vale package with custom rules and vocabulary for GitLab CI Utils projects.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/details&gt;
&lt;details&gt;&lt;summary&gt;Full list of 2024 software releases&lt;/summary&gt;
&lt;table class=&quot;first-column-350&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Releases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc&quot;&gt;Container Images / Cloc&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;8&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.3.0&quot;&gt;1.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.2.2&quot;&gt;1.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.2.1&quot;&gt;1.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.1.2&quot;&gt;1.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/cloc/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test&quot;&gt;Container Images / Go Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;19&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.6&quot;&gt;2.5.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.5&quot;&gt;2.5.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.4&quot;&gt;2.5.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.3&quot;&gt;2.5.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.2&quot;&gt;2.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.1&quot;&gt;2.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.5.0&quot;&gt;2.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.4.3&quot;&gt;2.4.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.4.2&quot;&gt;2.4.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.4.1&quot;&gt;2.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.4.0&quot;&gt;2.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.3.2&quot;&gt;2.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.3.1&quot;&gt;2.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.3.0&quot;&gt;2.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.2.1&quot;&gt;2.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.2.0&quot;&gt;2.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.1.1&quot;&gt;2.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools&quot;&gt;Container Images / Image Tools&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.2.1&quot;&gt;1.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer&quot;&gt;Container Images / Puppeteer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/14.0.0&quot;&gt;14.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/13.2.0&quot;&gt;13.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/13.1.2&quot;&gt;13.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/13.1.1&quot;&gt;13.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/13.1.0&quot;&gt;13.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/puppeteer/-/releases/13.0.0&quot;&gt;13.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft&quot;&gt;Container Images / Syft&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;7&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.5&quot;&gt;1.2.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.4&quot;&gt;1.2.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.3&quot;&gt;1.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.2&quot;&gt;1.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.1&quot;&gt;1.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale&quot;&gt;Container Images / Vale&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;49&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.1&quot;&gt;3.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.3.0&quot;&gt;3.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.2.1&quot;&gt;3.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.2.0&quot;&gt;3.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.1.0&quot;&gt;3.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.3.3&quot;&gt;2.3.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.3.2&quot;&gt;2.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.3.1&quot;&gt;2.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.3.0&quot;&gt;2.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.2.1&quot;&gt;2.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.2.0&quot;&gt;2.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.1.1&quot;&gt;2.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.13.1&quot;&gt;1.13.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.13.0&quot;&gt;1.13.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.12.0&quot;&gt;1.12.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.11.1&quot;&gt;1.11.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.11.0&quot;&gt;1.11.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.10.0&quot;&gt;1.10.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.9.1&quot;&gt;1.9.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.9.0&quot;&gt;1.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.8.0&quot;&gt;1.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.7.0&quot;&gt;1.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.6.0&quot;&gt;1.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.5.1&quot;&gt;1.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.5.0&quot;&gt;1.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.4.2&quot;&gt;1.4.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.4.1&quot;&gt;1.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.4.0&quot;&gt;1.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.3.3&quot;&gt;1.3.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.3.2&quot;&gt;1.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.3.1&quot;&gt;1.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.3.0&quot;&gt;1.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.7&quot;&gt;1.0.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.6&quot;&gt;1.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.5&quot;&gt;1.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.4&quot;&gt;1.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/vale/-/releases/0.5.0&quot;&gt;0.5.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard&quot;&gt;Node / StyleLint Config Standard&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/19.0.0&quot;&gt;19.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/18.0.1&quot;&gt;18.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/18.0.0&quot;&gt;18.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/17.0.1&quot;&gt;17.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/17.0.0&quot;&gt;17.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary&quot;&gt;References / GitLab Group Project Summary&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.0.4&quot;&gt;1.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/references/gitlab-group-project-summary/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test&quot;&gt;Tests / Container Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.4&quot;&gt;1.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/container-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/go-test&quot;&gt;Tests / Go Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/go-test/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/go-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test&quot;&gt;Tests / Node CJS Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-cjs-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test&quot;&gt;Tests / Node ESM Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/tests/node-esm-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester&quot;&gt;Bin Tester&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger&quot;&gt;CI Logger&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files&quot;&gt;Config Files&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;12&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.1.0&quot;&gt;11.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/11.0.0&quot;&gt;11.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.6.0&quot;&gt;10.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.5.0&quot;&gt;10.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.4.0&quot;&gt;10.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.3.0&quot;&gt;10.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.2.0&quot;&gt;10.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.1.1&quot;&gt;10.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.1.0&quot;&gt;10.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.0.2&quot;&gt;10.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.0.1&quot;&gt;10.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/10.0.0&quot;&gt;10.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check&quot;&gt;Docker Dependency Check&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;20&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.1.2&quot;&gt;4.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.1.1&quot;&gt;4.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.1.0&quot;&gt;4.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.9&quot;&gt;3.0.9&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.8&quot;&gt;3.0.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.7&quot;&gt;3.0.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.6&quot;&gt;3.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.5&quot;&gt;3.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.4&quot;&gt;3.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.3&quot;&gt;3.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.2.1&quot;&gt;2.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.2.0&quot;&gt;2.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.7&quot;&gt;2.0.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.6&quot;&gt;2.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.5&quot;&gt;2.0.5&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts&quot;&gt;Docker PowerShell Scripts&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/5.3.0&quot;&gt;5.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/5.2.0&quot;&gt;5.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/5.1.0&quot;&gt;5.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer&quot;&gt;Docker Puppeteer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/13.0.0&quot;&gt;13.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/12.0.0&quot;&gt;12.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/11.1.0&quot;&gt;11.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/11.0.1&quot;&gt;11.0.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates&quot;&gt;Docker Sokrates&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.4&quot;&gt;3.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.3&quot;&gt;3.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/2.1.2&quot;&gt;2.1.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq&quot;&gt;Docker curl jq&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;8&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.1.0&quot;&gt;3.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.0.5&quot;&gt;3.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.0.4&quot;&gt;3.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.0.3&quot;&gt;3.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard&quot;&gt;ESLint Config Standard&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;14&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/33.0.2&quot;&gt;33.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/33.0.1&quot;&gt;33.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/33.0.0&quot;&gt;33.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/32.0.0&quot;&gt;32.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/31.1.0&quot;&gt;31.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/31.0.1&quot;&gt;31.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/31.0.0&quot;&gt;31.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/30.0.0&quot;&gt;30.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/29.0.0&quot;&gt;29.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/28.0.0&quot;&gt;28.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/27.0.1&quot;&gt;27.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/27.0.0&quot;&gt;27.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/26.0.1&quot;&gt;26.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/26.0.0&quot;&gt;26.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env&quot;&gt;GitLab CI Env&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/10.1.0&quot;&gt;10.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/10.0.0&quot;&gt;10.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/9.3.0&quot;&gt;9.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/9.2.0&quot;&gt;9.2.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;GitLab CI Templates&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;36&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.3.1&quot;&gt;37.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.3.0&quot;&gt;37.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.2.0&quot;&gt;37.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.1.0&quot;&gt;37.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/37.0.0&quot;&gt;37.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/36.0.1&quot;&gt;36.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/36.0.0&quot;&gt;36.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/35.1.0&quot;&gt;35.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/35.0.0&quot;&gt;35.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/34.0.0&quot;&gt;34.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.8.0&quot;&gt;33.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.7.0&quot;&gt;33.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.6.0&quot;&gt;33.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.5.0&quot;&gt;33.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.4.0&quot;&gt;33.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.3.0&quot;&gt;33.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.2.0&quot;&gt;33.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.1.0&quot;&gt;33.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/33.0.0&quot;&gt;33.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/32.0.0&quot;&gt;32.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/31.1.0&quot;&gt;31.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/31.0.0&quot;&gt;31.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/30.0.0&quot;&gt;30.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.6.0&quot;&gt;29.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.5.0&quot;&gt;29.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.4.0&quot;&gt;29.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.3.0&quot;&gt;29.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.2.0&quot;&gt;29.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.1.0&quot;&gt;29.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/29.0.0&quot;&gt;29.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/28.1.0&quot;&gt;28.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/28.0.0&quot;&gt;28.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/27.0.1&quot;&gt;27.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/27.0.0&quot;&gt;27.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/26.1.1&quot;&gt;26.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/26.1.0&quot;&gt;26.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy&quot;&gt;GitLab Webhook Renovate Proxy&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;3&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.6.0&quot;&gt;0.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.5.0&quot;&gt;0.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy/-/releases/0.0.1&quot;&gt;0.0.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd&quot;&gt;GitLab PMD CPD&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;18&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.5.0&quot;&gt;2.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.4.1&quot;&gt;2.4.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.4.0&quot;&gt;2.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.6&quot;&gt;2.3.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.5&quot;&gt;2.3.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.4&quot;&gt;2.3.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.3&quot;&gt;2.3.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.2&quot;&gt;2.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.1&quot;&gt;2.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.3.0&quot;&gt;2.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.2.1&quot;&gt;2.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.2.0&quot;&gt;2.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.1.2&quot;&gt;2.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.1.1&quot;&gt;2.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.0.3&quot;&gt;2.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.0.2&quot;&gt;2.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci&quot;&gt;GitLab Pa11y CI&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;22&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.2.4&quot;&gt;11.2.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.2.3&quot;&gt;11.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.2.2&quot;&gt;11.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.2.1&quot;&gt;11.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.2.0&quot;&gt;11.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.1.0&quot;&gt;11.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/11.0.0&quot;&gt;11.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.1.0&quot;&gt;10.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.0.5&quot;&gt;10.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.0.4&quot;&gt;10.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.0.3&quot;&gt;10.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.0.2&quot;&gt;10.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.0.1&quot;&gt;10.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/10.0.0&quot;&gt;10.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/9.0.5&quot;&gt;9.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/9.0.4&quot;&gt;9.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/9.0.3&quot;&gt;9.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/9.0.2&quot;&gt;9.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/9.0.1&quot;&gt;9.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/9.0.0&quot;&gt;9.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/8.0.1&quot;&gt;8.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline&quot;&gt;GitLab Pipeline Timeline&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;3&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline/-/releases/0.4.0&quot;&gt;0.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline/-/releases/0.3.1&quot;&gt;0.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline/-/releases/0.3.0&quot;&gt;0.3.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser&quot;&gt;GitLab Releaser&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.4&quot;&gt;8.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.3&quot;&gt;8.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.2&quot;&gt;8.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.1&quot;&gt;8.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/7.0.2&quot;&gt;7.0.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus&quot;&gt;GitLab Semgrep Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;48&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.1.3&quot;&gt;9.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.1.2&quot;&gt;9.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.1.1&quot;&gt;9.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.1.0&quot;&gt;9.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.0.3&quot;&gt;9.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.0.2&quot;&gt;9.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.0.1&quot;&gt;9.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/9.0.0&quot;&gt;9.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.6.4&quot;&gt;8.6.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.6.3&quot;&gt;8.6.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.6.2&quot;&gt;8.6.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.6.1&quot;&gt;8.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.6.0&quot;&gt;8.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.7&quot;&gt;8.5.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.6&quot;&gt;8.5.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.5&quot;&gt;8.5.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.4&quot;&gt;8.5.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.3&quot;&gt;8.5.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.2&quot;&gt;8.5.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.1&quot;&gt;8.5.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.5.0&quot;&gt;8.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.4.0&quot;&gt;8.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.3.1&quot;&gt;8.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.3.0&quot;&gt;8.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.2.3&quot;&gt;8.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.2.2&quot;&gt;8.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.2.1&quot;&gt;8.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.2.0&quot;&gt;8.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.1.3&quot;&gt;8.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.1.2&quot;&gt;8.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.1.1&quot;&gt;8.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.1.0&quot;&gt;8.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.0.1&quot;&gt;8.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/7.0.1&quot;&gt;7.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/6.2.1&quot;&gt;6.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/6.2.0&quot;&gt;6.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/6.1.2&quot;&gt;6.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/6.1.1&quot;&gt;6.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/6.1.0&quot;&gt;6.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/5.2.3&quot;&gt;5.2.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/5.2.2&quot;&gt;5.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/5.2.1&quot;&gt;5.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/5.2.0&quot;&gt;5.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/5.1.0&quot;&gt;5.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse&quot;&gt;Lighthouse&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;34&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.2.2&quot;&gt;7.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.2.1&quot;&gt;7.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.2.0&quot;&gt;7.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.1.1&quot;&gt;7.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.1.0&quot;&gt;7.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.4.0&quot;&gt;6.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.3.2&quot;&gt;6.3.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.3.1&quot;&gt;6.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.3.0&quot;&gt;6.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.2.2&quot;&gt;6.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.2.1&quot;&gt;6.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.2.0&quot;&gt;6.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.1.2&quot;&gt;6.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.1.1&quot;&gt;6.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.1.0&quot;&gt;6.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.0.2&quot;&gt;6.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.0.1&quot;&gt;6.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/5.3.0&quot;&gt;5.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/5.2.0&quot;&gt;5.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/5.1.0&quot;&gt;5.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/5.0.1&quot;&gt;5.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.9.0&quot;&gt;4.9.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.8.2&quot;&gt;4.8.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.8.1&quot;&gt;4.8.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.8.0&quot;&gt;4.8.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.7.0&quot;&gt;4.7.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.6.2&quot;&gt;4.6.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.6.1&quot;&gt;4.6.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.6.0&quot;&gt;4.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.5.0&quot;&gt;4.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.4.0&quot;&gt;4.4.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary&quot;&gt;Pa11y CI CLI Summary Reporter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary/-/releases/4.0.1&quot;&gt;4.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html&quot;&gt;Pa11y CI HTML Reporter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner&quot;&gt;Pa11y CI Reporter Runner&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;8&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/5.0.3&quot;&gt;5.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/5.0.2&quot;&gt;5.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/5.0.1&quot;&gt;5.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/4.1.3&quot;&gt;4.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/4.1.2&quot;&gt;4.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/4.1.1&quot;&gt;4.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/4.1.0&quot;&gt;4.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus&quot;&gt;Pa11y HTML Reporter Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;3&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/4.0.1&quot;&gt;4.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean&quot;&gt;Pagean&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;39&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.1.0&quot;&gt;13.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.0.3&quot;&gt;13.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.0.2&quot;&gt;13.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.0.1&quot;&gt;13.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/13.0.0&quot;&gt;13.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.3.0&quot;&gt;12.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.2.1&quot;&gt;12.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.2.0&quot;&gt;12.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.1.3&quot;&gt;12.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.1.2&quot;&gt;12.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.1.1&quot;&gt;12.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.1.0&quot;&gt;12.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.0.3&quot;&gt;12.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.0.2&quot;&gt;12.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.0.1&quot;&gt;12.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/12.0.0&quot;&gt;12.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.2.1&quot;&gt;11.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.2.0&quot;&gt;11.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.1.5&quot;&gt;11.1.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.1.4&quot;&gt;11.1.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.1.3&quot;&gt;11.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.1.2&quot;&gt;11.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.1.1&quot;&gt;11.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.1.0&quot;&gt;11.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.0.4&quot;&gt;11.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.0.3&quot;&gt;11.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.0.2&quot;&gt;11.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.0.1&quot;&gt;11.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/11.0.0&quot;&gt;11.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.2.0&quot;&gt;10.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.1.5&quot;&gt;10.1.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.1.4&quot;&gt;10.1.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.1.3&quot;&gt;10.1.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.1.2&quot;&gt;10.1.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.1.1&quot;&gt;10.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.1.0&quot;&gt;10.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.0.2&quot;&gt;10.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.0.1&quot;&gt;10.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/10.0.0&quot;&gt;10.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog&quot;&gt;Releaselog&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;3&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/6.0.1&quot;&gt;6.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/5.0.1&quot;&gt;5.0.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config&quot;&gt;Renovate Config&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;3&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/renovate-config/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu&quot;&gt;Vale Rules for GitLab CI Utils&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.5&quot;&gt;1.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.4&quot;&gt;1.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/vale-package-gciu/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/details&gt;
&lt;p&gt;These releases included a handful of significant accomplishments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This work was started in 2023, but as of this year all of my NPM packages are
&lt;a href=&quot;https://github.blog/2023-04-19-introducing-npm-package-provenance/&quot;&gt;published with package provenance&lt;/a&gt;,
a step to help improve the overall security of the NPM package ecosystem. It&#39;s
an easy
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/signing_examples.html#use-sigstore-and-npm-to-generate-keyless-provenance&quot;&gt;one-time setup&lt;/a&gt;,
so there&#39;s no reason not to do it.&lt;/li&gt;
&lt;li&gt;I released my first multi-platform container image, built on GitLab with
&lt;a href=&quot;https://github.com/GoogleContainerTools/kaniko&quot;&gt;&lt;code&gt;kaniko&lt;/code&gt;&lt;/a&gt; and
&lt;a href=&quot;https://github.com/estesp/manifest-tool&quot;&gt;&lt;code&gt;manifest-tool&lt;/code&gt;&lt;/a&gt;, which uses
parent-child downstream pipelines to simplify expanding the supported
platforms. See the &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq&quot;&gt;&lt;code&gt;curl-jq&lt;/code&gt;&lt;/a&gt;
project for specifics.&lt;/li&gt;
&lt;li&gt;I released a script to add
&lt;a href=&quot;https://github.com/opencontainers/image-spec/blob/main/annotations.md&quot;&gt;OCI annotations&lt;/a&gt;
to container images. This uses
&lt;a href=&quot;https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md&quot;&gt;&lt;code&gt;crane&lt;/code&gt;&lt;/a&gt;
to update existing images in a container registry, matching image &lt;code&gt;LABEL&lt;/code&gt;s
where they exist, determining the base image from the &lt;code&gt;Dockerfile&lt;/code&gt;, and works
with any build tool. The Docker Official images are built with OCI
annotations, and those cascade to derived images, so if you&#39;re not updating
them your image manifests may have missing or incorrect information, and as
more tools use that data it can become an increasing problem. See the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/image-tools&quot;&gt;&lt;code&gt;image-tools&lt;/code&gt;&lt;/a&gt;
container image project for details.&lt;/li&gt;
&lt;li&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;GitLab CI Templates&lt;/a&gt;
project pipeline was updated to trigger multi-project downstream pipelines in
the appropriate subset of newly created test projects of common types (Node
ESM/CJS, Go, container images). This allows for automated testing of template
changes (for at least 90% of the templates), which is a significant
improvement over the previous manual testing process.&lt;/li&gt;
&lt;li&gt;As part of this year&#39;s goal of migrating all of my CLI-based Node projects to
ESM, I converted the &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean&quot;&gt;Pagean&lt;/a&gt;
project to ESM. It&#39;s still intentionally JavaScript, not TypeScript, but I get
all the type checking I need with JSDoc comments, which the project uses
already, and &lt;code&gt;tsc&lt;/code&gt; is perfectly happy checking types with that. And most
importantly, there&#39;s still no build step.&lt;/li&gt;
&lt;li&gt;My favorite project of the year came out of nowhere in the last week of the
year - the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-webhook-renovate-proxy&quot;&gt;GitLab Webhook Renovate Proxy&lt;/a&gt;.
This is a Cloudflare Worker that intercepts GitLab merge request event
webhooks, which are extremely noisy, filters out all except those that should
cause &lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt; to re-analyze a project, and
forwards to GitLab to trigger a CI pipeline. This allows projects to use
Renovate&#39;s standard
&lt;a href=&quot;https://gitlab.com/renovate-bot/renovate-runner&quot;&gt;GitLab runner&lt;/a&gt;, but get back
some of the responsiveness of the Renovate GitHub app. A blog post with the
implementation details and setup instructions will be coming.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;contributions-to-other-open-source-projects&quot; tabindex=&quot;-1&quot;&gt;Contributions to other open source projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2024/#contributions-to-other-open-source-projects&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I also made contributions to a few other open source projects this year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I made my first contributions to GitLab, both
&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140638&quot;&gt;documentation cleanup&lt;/a&gt;
as well as
&lt;a href=&quot;https://gitlab.com/gitlab-org/release-cli/-/merge_requests/180&quot;&gt;fixing documentation and tests&lt;/a&gt;
for the GitLab &lt;code&gt;release-cli&lt;/code&gt; application.&lt;/li&gt;
&lt;li&gt;I made my first contribution to the Trivy vulnerability scanner
&lt;a href=&quot;https://github.com/aquasecurity/trivy/pull/7735&quot;&gt;updating their GitLab report template&lt;/a&gt;
to include the operating system to improve parity with GitLab&#39;s
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/container_scanning/&quot;&gt;container scanning report&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;looking-forward-to-2025&quot; tabindex=&quot;-1&quot;&gt;Looking forward to 2025 &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2024/#looking-forward-to-2025&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Like so many years, it&#39;s going to be a wild ride…&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>A Better GitLab Code Quality - Part 1 - Goodbye CodeClimate</title>

    <link href="https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/" />
    <updated>2024-08-27T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/</id>
    <content type="html">&lt;p&gt;GitLab&#39;s Code Quality analysis template can be a convenient tool, but the
default CodeClimate-based solution has some significant drawbacks, and was
recently deprecated. This post, part 1 of a series, explores the security,
performance, and usability issues with the default Code Quality template, and
why its deprecation not a significant loss. Future parts of this series will
explore a more flexible, more adaptable, and more capable approach.&lt;/p&gt;
&lt;h2 id=&quot;gitlab-codeclimate-based-code-quality-is-deprecated&quot; tabindex=&quot;-1&quot;&gt;GitLab CodeClimate-based Code Quality is deprecated &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#gitlab-codeclimate-based-code-quality-is-deprecated&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;GitLab 17.3 announced the official
&lt;a href=&quot;https://docs.gitlab.com/ee/update/deprecations.html?removal_milestone=18.0#codeclimate-based-code-quality-scanning-will-be-removed&quot;&gt;deprecation of GitLab&#39;s CodeClimate-based Code Quality analysis&lt;/a&gt;,
with removal planned for GitLab 18.0. In the interim only limited updates are
planned in accordance with their
&lt;a href=&quot;https://docs.gitlab.com/ee/update/terminology.html#deprecation&quot;&gt;support policy for deprecated capabilities&lt;/a&gt;.
There is an &lt;a href=&quot;https://gitlab.com/groups/gitlab-org/-/epics/8790&quot;&gt;open epic&lt;/a&gt;
looking at future solutions, although it&#39;s notional and aspirational at this
point. Their plan is to keep Code Quality reports, and the existing UI
capabilities in Pipelines and Merge Requests, so custom Code Quality solutions
remain working as expected (which is the direction this series is going).&lt;/p&gt;
&lt;p&gt;The remainder of this post was originally written before this announcement, so
it&#39;s included here to provide additional context on why this is not a great
loss.&lt;/p&gt;
&lt;h2 id=&quot;problems-with-gitlab&#39;s-codeclimate-based-code-quality&quot; tabindex=&quot;-1&quot;&gt;Problems with GitLab&#39;s CodeClimate-based Code Quality &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#problems-with-gitlab&#39;s-codeclimate-based-code-quality&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;GitLab&#39;s CodeClimate-based Code Quality template can be a convenient and
effective tool for analyzing code quality, but it does have some significant
drawbacks including security concerns, performance issues, usability issues, and
analysis limitations.&lt;/p&gt;
&lt;h3 id=&quot;runner-security&quot; tabindex=&quot;-1&quot;&gt;Runner security &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#runner-security&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;CodeClimate requires one of two general configurations to perform the analysis
since it spawns containers as needed based on the analysis requirements. These
configurations expose potentially significant security concerns.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default template assumes Docker-in-Docker is used. This is a well
understood technique, but requires runners running in privileged mode.
Privileged mode gives the container all the &lt;code&gt;root&lt;/code&gt; capabilities of the host.
From the
&lt;a href=&quot;https://docs.gitlab.com/runner/security/index.html#usage-of-docker-executor&quot;&gt;GitLab docs&lt;/a&gt; -
&amp;quot;&lt;em&gt;When privileged mode is enabled, a user running a CI/CD job could gain full
root access to the runner’s host system, permission to mount and unmount
volumes, and run nested containers. By enabling privileged mode, you are
effectively disabling all the container’s security mechanisms and exposing
your host to privilege escalation, which can lead to container breakout.&lt;/em&gt;&amp;quot;&lt;/li&gt;
&lt;li&gt;The
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html#improve-code-quality-performance-with-private-runners&quot;&gt;alternative configuration&lt;/a&gt;
for private runners requires exposing the Docker daemon socket to the
container (required to launch new containers). By default, this is owned by
the &lt;code&gt;root&lt;/code&gt; user, again exposing &lt;code&gt;root&lt;/code&gt; access to the host.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These settings represent attack vectors that could lead to the compromise of any
CI job performed on the container, as well as compromise of the host running the
container and anything running on it. While there are methods to limit the
potential impacts - not running parallel CI jobs on a VM, running on ephemeral
infrastructure, running Docker in
&lt;a href=&quot;https://docs.gitlab.com/runner/executors/docker.html#use-rootless-docker-in-docker-with-restricted-privileged-mode&quot;&gt;rootless mode&lt;/a&gt;,
etc. - there&#39;s no way to completely mitigate them, they&#39;re just varying levels
of risk. There are also questions about the viability of some solutions, like
rootless mode, as
&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/issues/469064&quot;&gt;this issue&lt;/a&gt; suggests.&lt;/p&gt;
&lt;p&gt;Even limiting access to known users can lead to an exploit if they bring in
compromised external dependencies (and what application doesn&#39;t bring in
external dependencies in modern development). Because of that, the
recommendation to not allow configurations with these potential attack vectors
is well documented in container security best practice guides from across the
industry, including
&lt;a href=&quot;https://anchore.com/blog/docker-security-best-practices-a-complete-guide/&quot;&gt;Anchore&lt;/a&gt;,
&lt;a href=&quot;https://www.aquasec.com/blog/docker-security-best-practices/&quot;&gt;Aqua Security&lt;/a&gt;,
&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html&quot;&gt;OWASP Foundation&lt;/a&gt;,
&lt;a href=&quot;https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf&quot;&gt;NIST&lt;/a&gt;,
GitLab&#39;s own
&lt;a href=&quot;https://docs.gitlab.com/runner/security/index.html#usage-of-docker-executor&quot;&gt;Docker runner security risks&lt;/a&gt;,
and certainly others.&lt;/p&gt;
&lt;p&gt;Even &lt;a href=&quot;https://hub.docker.com/_/docker&quot;&gt;Docker Hub&#39;s Docker page&lt;/a&gt; recommends
reviewing
&lt;a href=&quot;https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/&quot;&gt;this article&lt;/a&gt;
to understand the risks before running Docker-in-Docker. This has been updated
with a more secure alternative, which has not been investigated thoroughly as
part of this evaluation. This may be a viable solution, although it would not
resolve the other issues identified here.&lt;/p&gt;
&lt;h3 id=&quot;speed&quot; tabindex=&quot;-1&quot;&gt;Speed &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#speed&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The GitLab Code Quality job is slow, with several contributing factors.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Docker-in-Docker startup cost (reduced if running
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html#improve-code-quality-performance-with-private-runners&quot;&gt;GitLab&#39;s alternative configuration&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The overhead of launching the various analysis containers.&lt;/li&gt;
&lt;li&gt;The time to download the various analysis containers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It can take several minutes even for a small project (e.g. &amp;lt; 500 lines of
JavaScript), which can quickly become the longest running job in a pipeline.
There are some mitigations to reduce these impacts. The first two performance
items in the preceding list are somewhat constrained by compute and memory
allocated to the runner. So, allocating more resources can reduce execution
time. For
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html&quot;&gt;gitlab.com shared SaaS runners&lt;/a&gt;,
this would mean moving to more capable runners - medium, large, x-large,
2x-large - although this does come at increased usage cost. Even with this,
there are limits to the effectiveness, and a single job running multiple
analyses is typically slower than running multiple parallel jobs.&lt;/p&gt;
&lt;p&gt;The time to download can be harder to improve since some of these containers are
large. To illustrate the magnitude of the problem, the following table shows the
container images and size to run Code Quality analysis configured for a
JavaScript-based web project (with &lt;code&gt;csslint&lt;/code&gt;, &lt;code&gt;duplication&lt;/code&gt;, &lt;code&gt;eslint&lt;/code&gt;, and
&lt;code&gt;fixme&lt;/code&gt;) enabled (which is the same as the default configuration, but with
&lt;code&gt;coffeelint&lt;/code&gt; and &lt;code&gt;rubocop&lt;/code&gt; disabled).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker:20.10.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;65 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker:20.10.12-dind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;registry.gitlab.com/gitlab-org/ci-cd/codequality:0.96.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;110 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-structure:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-csslint:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-duplication:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.34 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-eslint:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;170 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-fixme:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;21 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.14 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The following table shows the container images and size to run Code Quality
analysis on a Go-based project with an updated configuration enabling all of the
available Go plugins.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker:20.10.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;65 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker:20.10.12-dind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;registry.gitlab.com/gitlab-org/ci-cd/codequality:0.96.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;110 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-structure:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-duplication:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.34 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-fixme:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;21 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-golint:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;127 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-gofmt:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;101 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codeclimate/codeclimate-govet:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;125 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.26 GB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each of these examples includes a total of over 5 GB of container images.&lt;/p&gt;
&lt;p&gt;There are
&lt;a href=&quot;https://docs.gitlab.com/runner/configuration/speed_up_job_execution.html&quot;&gt;various container image caching strategies&lt;/a&gt;
that may help in some instances, but may not be available based on your runner
configuration.&lt;/p&gt;
&lt;h3 id=&quot;ease-of-use&quot; tabindex=&quot;-1&quot;&gt;Ease of use &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#ease-of-use&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html#enable-code-quality&quot;&gt;GitLab docs&lt;/a&gt;
give the impression that enabling their CodeClimate-based Code Quality analysis
is as simple as including the following in the project&#39;s &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Jobs/Code&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Quality.gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reality is that this only partially true. The
&lt;a href=&quot;https://gitlab.com/gitlab-org/ci-cd/codequality/-/blob/master/codeclimate_defaults/.codeclimate.yml.template&quot;&gt;default configuration&lt;/a&gt;
is used in this case, which runs the following plugins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;csslint&lt;/code&gt; (CSS)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;coffeelint&lt;/code&gt; (CoffeeScript)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;duplication&lt;/code&gt; (only for Ruby, JavaScript, Python, PHP)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eslint&lt;/code&gt; (JavaScript/TypeScript)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fixme&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rubocop&lt;/code&gt; (Ruby)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of these may not be applicable, for example the &lt;code&gt;coffeelint&lt;/code&gt; plugin is
probably past its useful life. There are also numerous other
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html#using-analysis-plugins&quot;&gt;analysis plugins&lt;/a&gt;,
but these need to be specifically configured with a &lt;code&gt;.codeclimate.yml&lt;/code&gt; file.
CodeClimate is not adaptable enough to check file types in the project and run
the applicable plugins or jobs as is done with other GitLab templates, for
example &lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/sast/&quot;&gt;&lt;code&gt;sast&lt;/code&gt;&lt;/a&gt; or
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/dependency_scanning/&quot;&gt;&lt;code&gt;dependency_scanning&lt;/code&gt;&lt;/a&gt;
(if using GitLab Ultimate).&lt;/p&gt;
&lt;h3 id=&quot;limited-analysis-engines&quot; tabindex=&quot;-1&quot;&gt;Limited analysis engines &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#limited-analysis-engines&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The complete list of CodeClimate analysis plugins can be found
&lt;a href=&quot;https://docs.codeclimate.com/docs/list-of-engines&quot;&gt;here&lt;/a&gt;. The officially
supported list of plugins is quite limited, and even with the community
supported plugins it&#39;s far from a comprehensive coverage of modern development
languages. Even in cases where languages are covered, the analysis engines (all
integrating other open source tools) are outdated. A couple of examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There&#39;s no question that &lt;code&gt;eslint&lt;/code&gt; is the solution for linting JavaScript, but
CodeClimate only supports up to
&lt;a href=&quot;https://github.com/codeclimate/codeclimate-eslint/blob/8ee4db9c3c83eb572b610003b671732b7300c4ac/package.json#L35C16-L35C22&quot;&gt;v8.50.0&lt;/a&gt;
(released September 2023), even though the latest v8 is v8.57.0 (released
February 2024), and v9 is out (released April 2024) and has 12 releases up to
v9.9.1 (as of this writing in August 2024). Given this release cadence with
valuable fixes and rule updates, the lag to integrate into CodeClimate is
intolerable (in this case a year behind).&lt;/li&gt;
&lt;li&gt;The available Go plugins are limited to
&lt;a href=&quot;https://docs.codeclimate.com/docs/gofmt&quot;&gt;&lt;code&gt;gofmt&lt;/code&gt;&lt;/a&gt;,
&lt;a href=&quot;https://docs.codeclimate.com/docs/govet&quot;&gt;&lt;code&gt;govet&lt;/code&gt;&lt;/a&gt;, and
&lt;a href=&quot;https://docs.codeclimate.com/docs/golint&quot;&gt;&lt;code&gt;golint&lt;/code&gt;&lt;/a&gt;. While the first two are
still used, &lt;code&gt;golint&lt;/code&gt; has been archived for years (and even the CodeClimate
plugin is deprecated). There&#39;s no support for modern engines like
&lt;a href=&quot;https://github.com/golangci/golangci-lint&quot;&gt;&lt;code&gt;golangci-lint&lt;/code&gt;&lt;/a&gt;, which itself has
dozens of linters covering almost anything you could want to lint (including
&lt;code&gt;gofmt&lt;/code&gt; and &lt;code&gt;govet&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;Summary &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/a-better-gitlab-code-quality---part-1--goodbye-codeclimate/#summary&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post has examined the various issues with GitLab&#39;s CodeClimate-based Code
Quality analysis, and why its deprecation is not a significant loss. Future
parts of this series will explore a more flexible, more adaptable, and more
capable approach.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Notifications for New Eleventy Posts in GitLab - Part 2</title>

    <link href="https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/" />
    <updated>2024-04-29T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/</id>
    <content type="html">&lt;p&gt;One of the challenges with deploying static sites is that there&#39;s nothing
tracking any sort of site state, including when new content is published. This
post presents a technique to identify newly published content on an Eleventy
site and sending various notifications with content-specific data.
&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/&quot;&gt;Part 1&lt;/a&gt; covered
identifying the new posts and collecting post-specific data. Part 2 covers
posting a status to Mastodon, posting a status to Bluesky, and sending an
IndexNow notification for the new page.&lt;/p&gt;
&lt;h2 id=&quot;sending-notifications-for-new-posts&quot; tabindex=&quot;-1&quot;&gt;Sending notifications for new posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#sending-notifications-for-new-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Once the new posts are identified, any kind of notification could be sent, but
the three implemented here are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Posting a status to Mastodon&lt;/li&gt;
&lt;li&gt;Posting a status to Bluesky&lt;/li&gt;
&lt;li&gt;Sending an &lt;a href=&quot;https://www.indexnow.org/&quot;&gt;IndexNow&lt;/a&gt; notification&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;managing-secrets-in-gitlab&quot; tabindex=&quot;-1&quot;&gt;Managing secrets in GitLab &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#managing-secrets-in-gitlab&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The following sections use secrets, specifically API keys/tokens, to access
those services securely. These secrets should &lt;strong&gt;not&lt;/strong&gt; be kept in the project&#39;s
git repository. For these examples, these are all stored as
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#define-a-cicd-variable-in-the-ui&quot;&gt;variables&lt;/a&gt;
in GitLab, which are encrypted for security. They also have the following
settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#protect-a-cicd-variable&quot;&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/a&gt;:
Ensures the variable is only exposed on
&lt;a href=&quot;https://docs.gitlab.com/ee/user/project/protected_branches.html&quot;&gt;protected branches&lt;/a&gt; or &lt;a href=&quot;https://docs.gitlab.com/ee/user/project/protected_tags.html&quot;&gt;protected tags&lt;/a&gt;.
By default, these require project &lt;code&gt;maintainer&lt;/code&gt; or &lt;code&gt;owner&lt;/code&gt; permissions to
execute and is typically the setting on the default branch.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable&quot;&gt;&lt;code&gt;masked&lt;/code&gt;&lt;/a&gt;:
Uses GitLab&#39;s bult-in logic to redact the variable in any GitLab CI job log.
As the GitLab docs note, this is on a best-effort basis, but is generally
reliable (maybe sometimes too reliable and there&#39;s unnecessary masking -
better to be safe than sorry).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These variables are then exposed to jobs on GitLab CI pipelines run on protected
branches or tags as environment variables. For this example that&#39;s the default
branch, which is where the site is published.&lt;/p&gt;
&lt;p&gt;See
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#cicd-variable-security&quot;&gt;the documentation&lt;/a&gt;
for additional details on GitLab variables security.&lt;/p&gt;
&lt;h2 id=&quot;iterate-through-new-posts-providing-notifications&quot; tabindex=&quot;-1&quot;&gt;Iterate through new posts providing notifications &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#iterate-through-new-posts-providing-notifications&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In &lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/&quot;&gt;part 1&lt;/a&gt;, a
function was introduced to iterates through new posts to send notifications via
an async queue.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getNewPosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;No new posts to submit&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; taskQueue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Submitting updates for &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        taskQueue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;postMastodonStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;postBlueskyStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;postIndexNow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allSettled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;taskQueue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; results&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;rejected&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function has been updated to call functions for each of the three desired
notifications passing the post data. The implementation for the
&lt;code&gt;postMastodonStatus&lt;/code&gt;, &lt;code&gt;postBlueskyStatus&lt;/code&gt;, and &lt;code&gt;postIndexNow&lt;/code&gt; functions is
detailed in the following sections. These are the three examples implemented
with this technique, but could be augmented or replaced with other
notifications.&lt;/p&gt;
&lt;h3 id=&quot;post-mastodon-status&quot; tabindex=&quot;-1&quot;&gt;Post Mastodon status &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#post-mastodon-status&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To start posting to your applicable Mastodon instance via the API, you must
first create an Application, and with that obtain an access token. This can be
done on your applicable Mastodon instance at Preferences &amp;gt; Development &amp;gt; New
application (at least it is in my case, the official documentation on doing this
via the UI is, unfortunately,
&lt;a href=&quot;https://docs.joinmastodon.org/user/contacts/#account&quot;&gt;limited&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;You&#39;ll need to provide application name, website, and the applicable scopes (you
do not need to modify the default Redirect URI value). The default scopes give
the application a lot of capability, for the actions here only &lt;code&gt;read:statuses&lt;/code&gt;
and &lt;code&gt;write:statuses&lt;/code&gt; are required. Then click SUBMIT and you&#39;ll be returned to
your list of applications. From there you can see the details for the
application that was just created, and &amp;quot;Your access token&amp;quot; is the token value to
use in the example below.&lt;/p&gt;
&lt;p&gt;The access token is stored as the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#protect-a-cicd-variable&quot;&gt;protected&lt;/a&gt;
and &lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable&quot;&gt;masked&lt;/a&gt;
variable &lt;code&gt;MASTODON_TOKEN&lt;/code&gt; within GitLab. The &lt;code&gt;postMastodonStatus&lt;/code&gt; function uses
that token and the &lt;code&gt;post&lt;/code&gt; information to post a status to Mastodon.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;postMastodonStatus&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; accessToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MASTODON_TOKEN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Update for the applicable Mastadon instance&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; instanceUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://fosstodon.org&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;instanceUrl&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/api/v1/statuses&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            Authorization&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;accessToken&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postStatus &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error posting Mastadon status: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;postStatus&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Mastodon status successfully posted (ID: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;postStatus&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function uses &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;post&lt;/code&gt; the status with the following data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The previously defined &lt;code&gt;MASTODON_TOKEN&lt;/code&gt; environment variable with the access
token.&lt;/li&gt;
&lt;li&gt;The base URL of the applicable Mastodon instance.&lt;/li&gt;
&lt;li&gt;A status message, which in this case includes the post description, URL, and
hashtags (the &lt;code&gt;tags&lt;/code&gt; array was previously formatted as hashtags).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If successful, a JSON response is returned and a success message is logged with
the returned status ID. If not, an error is thrown with the response error.&lt;/p&gt;
&lt;p&gt;Additional details on using the Mastodon API can be found in the
&lt;a href=&quot;https://docs.joinmastodon.org/client/intro/&quot;&gt;the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;post-bluesky-status&quot; tabindex=&quot;-1&quot;&gt;Post Bluesky status &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#post-bluesky-status&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To access Bluesky&#39;s API an App password must be created (please don&#39;t use your
actual account password). The
&lt;a href=&quot;https://docs.bsky.app/docs/get-started&quot;&gt;official docs&lt;/a&gt;, don&#39;t cover this. An
App password can be created at Settings &amp;gt; Advanced &amp;gt;
&lt;a href=&quot;https://bsky.app/settings/app-passwords&quot;&gt;App Passwords&lt;/a&gt; &amp;gt; Add App Password.
You&#39;ll be prompted for a name for the App, and then returned the password. Note
that this is only opportunity to view the password, so capture it somewhere
secure. If it&#39;s lost, it can be deleted and a replacement created.&lt;/p&gt;
&lt;p&gt;The App password is stored as the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#protect-a-cicd-variable&quot;&gt;protected&lt;/a&gt;
and &lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable&quot;&gt;masked&lt;/a&gt;
variable &lt;code&gt;BSKY_PASSWORD&lt;/code&gt; within GitLab. In addition the associated Bluesky ID is
stored as the protected and masked variable &lt;code&gt;BSKY_ID&lt;/code&gt; within GitLab.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;postBlueskyStatus&lt;/code&gt; function uses these credentials and the &lt;code&gt;post&lt;/code&gt;
information to submit a post to Bluesky.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; BskyAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; RichText &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;@atproto/api&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;postBlueskyStatus&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Use Bluesky agent based on API complexity&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; agent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BskyAgent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        service&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://bsky.social&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            identifier&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BSKY_ID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            password&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BSKY_PASSWORD&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Rich formatting in posts, for example links and mentions, must be&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// specified by byte offsets, so use the RichText capabilities to&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// detect them.&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RichText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; message &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; rt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;detectFacets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;agent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postRecord &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            $type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;app.bsky.feed.post&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            createdAt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// Cards are not automatically created from OG tags in the link,&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// they must be explicitly added. Note this does not include&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// images, which must be referenced, and separately posted to&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// the API.&lt;/span&gt;
            embed&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                $type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;app.bsky.embed.external&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                external&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    uri&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            facets&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; rt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;facets&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; rt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postRecord&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Bluesky status successfully posted&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error posting Bluesky status: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bluesky&#39;s &lt;a href=&quot;https://atproto.com/&quot;&gt;AT protocol&lt;/a&gt; has granular control, but with
that comes additional complexity. So, the Bluesky agent from the official
&lt;a href=&quot;https://www.npmjs.com/package/@atproto/api&quot;&gt;API client&lt;/a&gt; is used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;BSKY_ID&lt;/code&gt; and &lt;code&gt;BSKY_PASSWORD&lt;/code&gt; environment variable are used to login.&lt;/li&gt;
&lt;li&gt;The message includes the post description and URL. Any links (or mentions) in
the message are not automatically detected, and must be specified by the
start/end byte offset, but the API client provides the &lt;code&gt;RichText&lt;/code&gt; class that
is used to detect and provide this data (to &lt;em&gt;linkify&lt;/em&gt; the URL).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;postRecord&lt;/code&gt; is constructed with all of this data. In addition, &lt;em&gt;cards&lt;/em&gt;
representing the URL are not automatically added, so an &lt;code&gt;embed&lt;/code&gt; is included
with the card data (description, title, and URL). In this case an image is not
provided, although if required it must be submitted via the API as a separate
request (see
&lt;a href=&quot;https://docs.bsky.app/docs/advanced-guides/posts#website-card-embeds&quot;&gt;the docs&lt;/a&gt;
for full details).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If successful, a success message is logged. If not, an error is thrown with the
response error.&lt;/p&gt;
&lt;p&gt;Additional details on using the AT protocol for Posts can be found in the
&lt;a href=&quot;https://docs.bsky.app/docs/advanced-guides/posts&quot;&gt;the Bluesky documentation&lt;/a&gt;,
as well as the
&lt;a href=&quot;https://www.npmjs.com/package/@atproto/api&quot;&gt;API client documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;post-url-to-indexnow&quot; tabindex=&quot;-1&quot;&gt;Post URL to IndexNow &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#post-url-to-indexnow&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.indexnow.org/&quot;&gt;IndexNow&lt;/a&gt; is a standard that allows submission of a
URL to enabled search engines, which is then shared with all IndexNow-enabled
engines (Bing, for example, is one of the IndexNow enabled search engines). This
avoids an indexing delay waiting for the new page to be organically discovered.&lt;/p&gt;
&lt;p&gt;Details on using the API and obtaining an API key is available in the
&lt;a href=&quot;https://www.indexnow.org/documentation&quot;&gt;documentation&lt;/a&gt;. The API key is stored
as the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#protect-a-cicd-variable&quot;&gt;protected&lt;/a&gt;
and &lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable&quot;&gt;masked&lt;/a&gt;
variable &lt;code&gt;INDEXNOW_API_KEY&lt;/code&gt; within GitLab. To validate the API key, a file is
required to be deployed to the site. This file is named with the key, and its
content includes the key, so the previously defined &lt;code&gt;pages&lt;/code&gt; CI job &lt;code&gt;script&lt;/code&gt; is
updated to create the file.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  script&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# build runs `npx @11ty/eleventy`&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
    &lt;span class=&quot;token comment&quot;&gt;# Create IndexNow key file in the site output folder&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; echo $INDEXNOW_API_KEY &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; ./public/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;INDEXNOW_API_KEY&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.txt
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Ensure artifacts with the key file are not publicly visible&lt;/span&gt;
    public&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This implementation allows the API key to be securely stored in a GitLab
variable and the file created only for deployment. With that, the job
&lt;code&gt;artifacts&lt;/code&gt; is also updated with &lt;code&gt;public: false&lt;/code&gt; to ensure that job artifacts
are not made available publicly (only to project members). The variable is also
masked, but this provides an extra layer of protection against accidental
exposure.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;postIndexNow&lt;/code&gt; function uses the API key and the &lt;code&gt;post&lt;/code&gt; information to
submit the URL to IndexNow.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;postIndexNow&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; apiKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;INDEXNOW_API_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; indexNowUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://api.indexnow.org/IndexNow&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; host &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; postUrl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        host&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        key&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; apiKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        keyLocation&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;host&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;apiKey&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.txt&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        urlList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;postUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;indexNowUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error submitting URL to IndexNow: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;statusText&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;URL &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; successfully submitted to IndexNow&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function uses &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;post&lt;/code&gt; the status with the following data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The previously defined &lt;code&gt;INDEXNOW_API_KEY&lt;/code&gt; environment variable with the API
key.&lt;/li&gt;
&lt;li&gt;The website &lt;code&gt;host&lt;/code&gt; is taken from the new post URL.&lt;/li&gt;
&lt;li&gt;The location of the API key file that was previously created.&lt;/li&gt;
&lt;li&gt;The list of URLs (in this case only one URL is submitted with each request,
but this API endpoint does accept a list of URLs).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this cases the update is posted to &lt;code&gt;api.indexnow.org&lt;/code&gt;, although it could be
posted to another IndexNow server (for example, &lt;code&gt;www.bing.com&lt;/code&gt;) per the
documentation.&lt;/p&gt;
&lt;p&gt;If successful, a success message is logged. If not, an error is thrown with the
response status.&lt;/p&gt;
&lt;h2 id=&quot;posting-updates&quot; tabindex=&quot;-1&quot;&gt;Posting updates &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#posting-updates&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;On each commit to &lt;code&gt;main&lt;/code&gt;, the update script &lt;code&gt;./scripts/new-posts.js&lt;/code&gt; is run, as
detailed in
&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/&quot;&gt;part 1&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If no new posts are detected, the logs show:&lt;/p&gt;
&lt;pre class=&quot;language-shell-session&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-shell-session&quot;&gt;&lt;span class=&quot;token command&quot;&gt;&lt;span class=&quot;token shell-symbol important&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;token bash language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; ./scripts/new-posts.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token output&quot;&gt;No new posts to submit
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there is a new post, the logs show (the logs from part 1 of this post):&lt;/p&gt;
&lt;pre class=&quot;language-shell-session&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-shell-session&quot;&gt;&lt;span class=&quot;token command&quot;&gt;&lt;span class=&quot;token shell-symbol important&quot;&gt;$&lt;/span&gt; &lt;span class=&quot;token bash language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; ./scripts/new-posts.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token output&quot;&gt;Submitting updates for https://[MASKED]/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/
Mastodon status successfully posted (ID: 123456789012345678)
Bluesky status successfully posted
URL https://[MASKED]/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/ successfully submitted to IndexNow
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case the domain is &lt;code&gt;[MASKED]&lt;/code&gt; because it happens to be my Bluesky ID
(the value for &lt;code&gt;BSKY_ID&lt;/code&gt;, a masked variable that probably doesn&#39;t need to be
masked).&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;Summary &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-2/#summary&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post has detailed how to identify new posts in a Eleventy build, with
summary data for those posts, and use that to post updates to Mastodon, Bluesky,
and IndexNow. For reference, the final &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, &lt;code&gt;.eleventy.js&lt;/code&gt;, and
&lt;code&gt;./scripts/new-posts.js&lt;/code&gt; files (the pieces covered in this post) are included
here.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;npm_install&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  image&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  needs&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  script&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm ci
  artifacts&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    paths&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node_modules/

&lt;span class=&quot;token comment&quot;&gt;# Current sitemap must be retrieved before deploy for comparison.&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;get_current_sitemap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token comment&quot;&gt;# With no needs, the job will run at the start of the pipeline&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; wget &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;O sitemap.xml https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//&amp;lt;site&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;/sitemap.xml
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Site only deploys on the default branch&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; sitemap.xml

&lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm_install
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# build runs `npx @11ty/eleventy`&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
    &lt;span class=&quot;token comment&quot;&gt;# Create IndexNow key file in the site output folder&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; echo $INDEXNOW_API_KEY &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; ./public/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;INDEXNOW_API_KEY&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.txt
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Ensure artifacts with the key file are not publicly visible&lt;/span&gt;
    public&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; public/
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; posts.json

&lt;span class=&quot;token key atrule&quot;&gt;new_post_notification&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token comment&quot;&gt;# Needs specifies artifacts to download as well as prerequisite jobs&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Provides node_modules folder&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm_install
    &lt;span class=&quot;token comment&quot;&gt;# Provides previous sitemap.xml&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; get_current_sitemap
    &lt;span class=&quot;token comment&quot;&gt;# Provides built site and posts.json&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; pages
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node ./scripts/new&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;posts.js&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.eleventy.js&lt;/code&gt; (unchanged from part 1)&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&#39;use strict&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Global paths&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;src&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;sanitizeTag&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    tag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;[#/]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// other configuration&lt;/span&gt;

    eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;stringify&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;stringifyTags&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
        &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sanitizeTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tag&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Create collection of posts data for use in external notifications&lt;/span&gt;
    eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addCollection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;postsData&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
        collection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getFilteredByTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;posts&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            inputPath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            outputPath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            tags&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; tag &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Move the posts.json file to the root folder since not deployed&lt;/span&gt;
    eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;eleventy.after&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsDataFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;renameSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; postsDataFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; postsDataFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        dir&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            output&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; outputPath
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// other configuration&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;./scripts/new-posts.js&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&#39;use strict&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; BskyAgent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; RichText &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;@atproto/api&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemapFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;sitemap.xml&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsThreshold &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getNewPosts&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sitemapFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; urlRegex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;&amp;lt;loc&gt;(?&amp;lt;url&gt;.+&#92;/posts&#92;/.+?)&amp;lt;&#92;/loc&gt;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemapUrls &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;sitemap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;matchAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;urlRegex&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;groups&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postsFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; postsThreshold
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&#39;Error: sitemap and posts data are invalid or out of sync&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;postMastodonStatus&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; accessToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MASTODON_TOKEN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Update for the applicable Mastadon instance&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; instanceUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://fosstodon.org&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;instanceUrl&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/api/v1/statuses&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            Authorization&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;accessToken&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postStatus &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error posting Mastadon status: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;postStatus&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Mastodon status successfully posted (ID: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;postStatus&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;postBlueskyStatus&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Use Bluesky agent based on API complexity&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; agent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BskyAgent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        service&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://bsky.social&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            identifier&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BSKY_ID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            password&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BSKY_PASSWORD&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&#92;n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Rich formatting in posts, for example links and mentions, must be&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// specified by byte offsets, so use the RichText capabilities to&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// detect them.&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RichText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; message &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; rt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;detectFacets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;agent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postRecord &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            $type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;app.bsky.feed.post&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            createdAt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// Cards are not automatically created from OG tags in the link,&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// they must be explicitly added. Note this does not include&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// images, which must be referenced, and separately posted to&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// the API.&lt;/span&gt;
            embed&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                $type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;app.bsky.embed.external&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                external&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    uri&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            facets&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; rt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;facets&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; rt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;text
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; agent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postRecord&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Bluesky status successfully posted&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error posting Bluesky status: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;postIndexNow&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; apiKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;INDEXNOW_API_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; indexNowUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://api.indexnow.org/IndexNow&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postUrl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; host &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; postUrl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        host&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        key&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; apiKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        keyLocation&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;host&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;apiKey&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.txt&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        urlList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;postUrl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;indexNowUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        headers&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string-property property&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error submitting URL to IndexNow: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;statusText&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;URL &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; successfully submitted to IndexNow&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getNewPosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;No new posts to submit&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; taskQueue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Submitting updates for &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        taskQueue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;postMastodonStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;postBlueskyStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;postIndexNow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allSettled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;taskQueue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; results&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;rejected&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
</content>
    </entry>
    
    <entry>
    <title>Notifications for New Eleventy Posts in GitLab - Part 1</title>

    <link href="https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/" />
    <updated>2024-04-08T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/</id>
    <content type="html">&lt;p&gt;One of the challenges with deploying static sites is that there&#39;s nothing
tracking any sort of site state, including when new content is published. This
post presents a technique to identify newly published content on an Eleventy
site and sending various notifications with content-specific data. Part 1 covers
identifying the new posts and collecting post-specific data.&lt;/p&gt;
&lt;h2 id=&quot;the-example-case&quot; tabindex=&quot;-1&quot;&gt;The example case &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#the-example-case&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This example is for an Eleventy site derived from the
&lt;a href=&quot;https://github.com/11ty/eleventy-base-blog&quot;&gt;&lt;code&gt;eleventy-base-blog&lt;/code&gt;&lt;/a&gt; and sends
notifications for new published &lt;em&gt;posts&lt;/em&gt;. You&#39;ll see that limiting to posts is
merely filtering down the content and could be easily changed, but in this case
it&#39;s used to avoid cases like notifications for a new tag added to a post (which
could create a new tag-specific page for previously unused tags).&lt;/p&gt;
&lt;p&gt;The example also runs in GitLab CI and publishes to GitLab pages without
resorting to git diffs, using the GitLab API, or similar state comparisons to
determine newly published content. While this specific case is for an Eleventy
site hosted on GitLab pages, but the technique should be applicable to any CI
system.&lt;/p&gt;
&lt;p&gt;At a high level, the overall sequence is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#determine-current-posts&quot;&gt;Determine current posts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#get-data-for-all-posts&quot;&gt;Get data for all posts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#determine-new-posts&quot;&gt;Determine new posts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#provide-notifications-for-new-posts&quot;&gt;Provide notifications for new posts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;determine-current-posts&quot; tabindex=&quot;-1&quot;&gt;Determine current posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#determine-current-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In order to determine &lt;em&gt;new&lt;/em&gt; posts, there must be a source of truth for current
posts for comparison. The CI pipeline itself doesn&#39;t maintain state data for the
published site. It could potentially be obtained from the GitLab API if the
artifacts have not expired, which may not be a good assumption. There is one
straightforward source of truth, though - the &lt;code&gt;sitemap.xml&lt;/code&gt; file on the deployed
site. This assumes the site is providing a sitemap, which there are many reasons
to do, and that the sitemap only contains URLs and is not a sitemap index file
(linking to other sitemaps). If needed, there&#39;s an example
&lt;a href=&quot;https://github.com/11ty/eleventy-base-blog/blob/v8.0.0/content/sitemap/sitemap.xml.njk&quot;&gt;&lt;code&gt;sitemap.xml&lt;/code&gt;&lt;/a&gt;
in the &lt;code&gt;eleventy-base-blog&lt;/code&gt; for a reference implementation.&lt;/p&gt;
&lt;p&gt;For this case, a job is added to the CI pipeline before the new site is deployed
to retrieve the currently deployed site&#39;s &lt;code&gt;sitemap.xml&lt;/code&gt; and save as an artifact
for later use.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Current sitemap must be retrieved before deploy for comparison.&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;get_current_sitemap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token comment&quot;&gt;# With no needs, the job will run at the start of the pipeline&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; wget &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;O sitemap.xml https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//&amp;lt;site&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;/sitemap.xml
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Site only deploys on the default branch&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; sitemap.xml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This job has &lt;code&gt;rules&lt;/code&gt; configured to only run on the default branch since that
matches when the site is deployed. In this job &lt;code&gt;wget&lt;/code&gt; is used instead of &lt;code&gt;curl&lt;/code&gt;
simply because it&#39;s already installed in the &lt;code&gt;alpine&lt;/code&gt; image.&lt;/p&gt;
&lt;h2 id=&quot;get-data-for-all-posts&quot; tabindex=&quot;-1&quot;&gt;Get data for all posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#get-data-for-all-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To get the data for all posts, the Eleventy build is updated to create a JSON
file with all applicable data, which is then saved and made available for
subsequent jobs to use.&lt;/p&gt;
&lt;h3 id=&quot;create-posts-summary-json-file&quot; tabindex=&quot;-1&quot;&gt;Create posts summary JSON file &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#create-posts-summary-json-file&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To simplify creating the posts summary file, a new Eleventy
&lt;a href=&quot;https://www.11ty.dev/docs/collections/&quot;&gt;collection&lt;/a&gt; is added to the Eleventy
configuration.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Create collection of posts data for use in external notifications&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addCollection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;postsData&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    collection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getFilteredByTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;posts&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        inputPath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        outputPath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        tags&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; tag &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This collection gets data for all &lt;code&gt;posts&lt;/code&gt; (any page with the tag &lt;code&gt;posts&lt;/code&gt;), and
then creates a summary of post-related data to be used for notifications.
Collections are processed after all pages and the data cascade, so have access
to all page data, the rendered page, input and output file paths, etc. The
&lt;code&gt;tags&lt;/code&gt; value is updated to remove the &lt;code&gt;posts&lt;/code&gt; tag, leaving only other tags. In
this cases the &lt;code&gt;inputPath&lt;/code&gt; and &lt;code&gt;outputPath&lt;/code&gt; are included simply to illustrate
that the data is available, and could be included if those files needed to be
accessed to get some information (although a goal was to avoid that and use the
data that the collection already exposes).&lt;/p&gt;
&lt;p&gt;This site uses
&lt;a href=&quot;https://www.npmjs.com/package/eleventy-plugin-validate&quot;&gt;&lt;code&gt;eleventy-plugin-validate&lt;/code&gt;&lt;/a&gt;,
which ensures that the required post data (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;) is
available and match the required schema or the build fails. Therefore, the
&lt;code&gt;postsData&lt;/code&gt; collection does not perform duplicate data validation, although
that&#39;s recommended if the data is not already validated.&lt;/p&gt;
&lt;p&gt;In addition to the collection, two filters are added to the Eleventy
configuration.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;stringify&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;sanitizeTag&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    tag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;[#/&quot;]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;stringifyTags&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sanitizeTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tag&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;stringify&lt;/code&gt; filter is used to properly encode some data fields for the JSON
file (for example, double quotes that may be in a &lt;code&gt;title&lt;/code&gt; or &lt;code&gt;description&lt;/code&gt;
field, which would otherwise result in invalid JSON).&lt;/p&gt;
&lt;p&gt;The post tags is used to create hashtags, so the &lt;code&gt;stringifyTags&lt;/code&gt; filter takes
the array of post tags, encodes them, and prepends a &lt;code&gt;#&lt;/code&gt; to make valid hashtags.&lt;/p&gt;
&lt;p&gt;The JSON file itself is created with a new Nunjucks template (Nunjucks is used
across this site, this could use any template format).&lt;/p&gt;
&lt;pre class=&quot;language-njk&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-njk&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;permalink&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;posts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;json&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;eleventyExcludeFromCollections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;collections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;postsData&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ post.url | htmlBaseUrl(metadata.url) }}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;stringify&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;stringify&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;date&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ post.date | dateToRfc3339 }}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;inputPath&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ post.inputPath }}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;outputPath&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{{ post.outputPath }}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;stringifyTags&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;last&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endif&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This template iterates through the collection that was previously created to
generate a JSON array of posts data. There are three noteworthy items in the
template:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;permalink&lt;/code&gt; is used to set the filename, and the file is excluded from other
collections with &lt;code&gt;eleventyExcludeFromCollections&lt;/code&gt;. This is similar to how a
sitemap or RSS feed would be generated.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; fields call the &lt;code&gt;stringify&lt;/code&gt; filter. As was seen
previously, this calls &lt;code&gt;JSON.stringify&lt;/code&gt; on the value, which ensures it returns
a quoted string, so the returned values are not quoted in the template.&lt;/li&gt;
&lt;li&gt;All fields that call &lt;code&gt;stringify&lt;/code&gt; or &lt;code&gt;stringifyTags&lt;/code&gt; for encoding use the
&lt;code&gt;safe&lt;/code&gt; filter so they&#39;re not HTML encoded. The intent is to encode them as
valid JSON, not valid HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Eleventy build saves the &lt;code&gt;posts.json&lt;/code&gt; file in the site&#39;s &lt;code&gt;outputPath&lt;/code&gt; (set
previously in the &lt;code&gt;outputPath&lt;/code&gt; variable), so an &lt;code&gt;eleventy.after&lt;/code&gt; event is used
to copy the file to the root directory so it&#39;s not deployed with the site (not
an issue necessarily, but unnecessary).&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;eleventy.after&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsDataFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;renameSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; postsDataFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; postsDataFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, the &lt;code&gt;pages&lt;/code&gt; job &lt;code&gt;artifacts:paths&lt;/code&gt; is updated to save the new file in
addition to the site directory (in this case &lt;code&gt;public/&lt;/code&gt;).&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm_install
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# build runs `npx @11ty/eleventy`&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; public/
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; posts.json&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: this project uses the &lt;code&gt;npm_install&lt;/code&gt; job to install dependencies once
(via &lt;code&gt;npm ci&lt;/code&gt;) and save them as artifacts for all subsequent jobs where they
are required. The specific template can be found
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Npm-Install.gitlab-ci.yml&quot;&gt;here&lt;/a&gt;
for reference.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;determine-new-posts&quot; tabindex=&quot;-1&quot;&gt;Determine new posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#determine-new-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;New posts are determined by comparing the sitemap and posts summary files.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemapFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;sitemap.xml&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsThreshold &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getNewPosts&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sitemapFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; urlRegex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;&amp;lt;loc&gt;(?&amp;lt;url&gt;.+&#92;/posts&#92;/.+?)&amp;lt;&#92;/loc&gt;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemapUrls &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;sitemap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;matchAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;urlRegex&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;groups&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postsFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; postsThreshold
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&#39;Error: sitemap and posts data are invalid or out of sync&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sitemap file is read, and uses a regular expression to find all &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; tags
with &lt;code&gt;/posts/&lt;/code&gt; in the URL (rather than parsing the XML and then filtering the
results). The URL is stored in the &lt;code&gt;url&lt;/code&gt; named capture group. This is
implemented on a site with existing posts, so URLs should always be found in the
sitemap, and in the &lt;code&gt;posts.json&lt;/code&gt; file. As a check for any issues, an error is
thrown if either of those lists are empty (a sign of some problem), or different
by more than 3 posts. The latter is arbitrary, and could probably be narrowed to
1, but was implemented as another check that there wasn&#39;t an issue processing
data from either file. In my nominal workflow there should only be one new post
in any pipeline. There could also be new &lt;code&gt;tag&lt;/code&gt; pages with the blog template, but
these are not included in &lt;code&gt;posts&lt;/code&gt;. The two files are compared to determine the
new posts and an array of posts data for those posts is returned.&lt;/p&gt;
&lt;h2 id=&quot;provide-notifications-for-new-posts&quot; tabindex=&quot;-1&quot;&gt;Provide notifications for new posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#provide-notifications-for-new-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A new job &lt;code&gt;new_post_notification&lt;/code&gt; was created to run the script that checks for
new posts and sends notifications. This is separated to isolate notification
errors from pages generation and deploy errors, although these could be combined
into one job.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;new_post_notification&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token comment&quot;&gt;# Needs specifies artifacts to download as well as prerequisite jobs&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Provides node_modules folder&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm_install
    &lt;span class=&quot;token comment&quot;&gt;# Provides previous sitemap.xml&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; get_current_sitemap
    &lt;span class=&quot;token comment&quot;&gt;# Provides built site and posts.json&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; pages
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node ./scripts/new&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;posts.js&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As noted, this &lt;code&gt;needs&lt;/code&gt; the jobs that previously generated the applicable data
and the &lt;code&gt;node_modules&lt;/code&gt; folder for modules used by the &lt;code&gt;./scripts/new-posts.js&lt;/code&gt;
script.&lt;/p&gt;
&lt;p&gt;This site is using Eleventy v2 with CommonJS modules, so the main logic is
encapsulated in an IIFE to emulate top level await.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getNewPosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;No new posts to submit&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; taskQueue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Submitting updates for &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        taskQueue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// send updates for new posts&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allSettled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;taskQueue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; results&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;rejected&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It uses the previously discussed &lt;code&gt;getNewPosts&lt;/code&gt; function to get the summary data
for the new posts, the iterates through those posts to send notifications. All
tasks are pushed to an async queue and &lt;code&gt;Promise.allSettled&lt;/code&gt; is used to ensure
all tasks are executed, even on failure. If any of the tasks fail, an error is
logged.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;Summary &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/notifications-for-new-eleventy-posts-in-gitlab-part-1/#summary&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post has detailed how to identify new posts in a Eleventy build, with
summary data for those posts. For reference, the complete &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;,
&lt;code&gt;.eleventy.js&lt;/code&gt;, and &lt;code&gt;./scripts/new-posts.js&lt;/code&gt; files (the pieces covered in this
post) are included here.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;npm_install&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  image&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  needs&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  script&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm ci
  artifacts&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    paths&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node_modules/

&lt;span class=&quot;token comment&quot;&gt;# Current sitemap must be retrieved before deploy for comparison.&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;get_current_sitemap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token comment&quot;&gt;# With no needs, the job will run at the start of the pipeline&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; wget &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;O sitemap.xml https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//&amp;lt;site&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;/sitemap.xml
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Site only deploys on the default branch&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; sitemap.xml

&lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm_install
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# build runs `npx @11ty/eleventy`&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; public/
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; posts.json

&lt;span class=&quot;token key atrule&quot;&gt;new_post_notification&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;20&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token comment&quot;&gt;# Needs specifies artifacts to download as well as prerequisite jobs&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Provides node_modules folder&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm_install
    &lt;span class=&quot;token comment&quot;&gt;# Provides previous sitemap.xml&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; get_current_sitemap
    &lt;span class=&quot;token comment&quot;&gt;# Provides built site and posts.json&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; pages
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node ./scripts/new&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;posts.js&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.eleventy.js&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&#39;use strict&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Global paths&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;src&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;sanitizeTag&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    tag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;[#/]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// other configuration&lt;/span&gt;

    eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;stringify&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addFilter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;stringifyTags&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
        &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sanitizeTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tag&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

 &lt;span class=&quot;token comment&quot;&gt;// Create collection of posts data for use in external notifications&lt;/span&gt;
 eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addCollection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;postsData&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
     collection&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getFilteredByTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;posts&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         date&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         inputPath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         outputPath&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         tags&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tags&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; tag &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url
     &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Move the posts.json file to the root folder since not deployed&lt;/span&gt;
 eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;eleventy.after&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsDataFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;renameSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; postsDataFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; postsDataFilename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        dir&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; inputPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            output&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; outputPath
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// other configuration&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;./scripts/new-posts.js&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&#39;use strict&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;node:fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemapFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;sitemap.xml&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsFilename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;posts.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postsThreshold &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getNewPosts&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sitemapFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; urlRegex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;&amp;lt;loc&gt;(?&amp;lt;url&gt;.+&#92;/posts&#92;/.+?)&amp;lt;&#92;/loc&gt;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sitemapUrls &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;sitemap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;matchAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;urlRegex&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;groups&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postsFilename&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt;
        Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; postsThreshold
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&#39;Error: sitemap and posts data are invalid or out of sync&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;sitemapUrls&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; posts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getNewPosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;posts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;No new posts to submit&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; taskQueue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; post &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; posts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Submitting updates for &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        taskQueue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// send updates for new posts&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;allSettled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;taskQueue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; results&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;rejected&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reason&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Details for the currently implemented notifications will be discussed in Part 2:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Posting a status to Mastodon&lt;/li&gt;
&lt;li&gt;Posting a status to Bluesky&lt;/li&gt;
&lt;li&gt;Sending an &lt;a href=&quot;https://www.indexnow.org/&quot;&gt;IndexNow&lt;/a&gt; notification&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
    
    <entry>
    <title>GitLab Code Quality Duplication Analysis With PMD CPD</title>

    <link href="https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/" />
    <updated>2024-03-11T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/</id>
    <content type="html">&lt;p&gt;This post describes an alternative to the built-in GitLab Code Quality duplicate
or similar code analysis, which uses Code Climate. It details four significant
issues observed with the Code Climate analysis and an assessment of alternative
tools for duplication analysis. This lead to the creation of a new project using
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd.html&quot;&gt;PMD Copy/Paste Detector (CPD)&lt;/a&gt;
that returns a GitLab Code Quality formatted report.&lt;/p&gt;
&lt;h2 id=&quot;issues-with-code-climate-duplication-analysis&quot; tabindex=&quot;-1&quot;&gt;Issues with Code Climate duplication analysis &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#issues-with-code-climate-duplication-analysis&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Experience with the built-in GitLab Code Quality duplication analysis, performed
by Code Climate, has shown the results to have several categories of issues.
Before diving into them, the following is a brief summary of how that analysis
is performed. Per the Code Climate
&lt;a href=&quot;https://docs.codeclimate.com/docs/duplication-concept&quot;&gt;documentation&lt;/a&gt;, the code
is parsed into an abstract syntax tree (AST), and each node of the tree is given
a &lt;em&gt;mass&lt;/em&gt; based on the number of sub nodes. Code Climate compares each node to
other nodes to find duplicates, and reports those greater than the applicable
mass threshold (&lt;em&gt;duplicate code&lt;/em&gt; or &lt;em&gt;similar code&lt;/em&gt;). Nodes that have the same
structure and values are &lt;em&gt;duplicate code&lt;/em&gt;, and those with the same structure but
different values are &lt;em&gt;similar code&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The following sections detail four categories of incomplete, incorrect, or
non-intuitive results. These sections compare some simplified JavaScript code
samples for brevity, and where appropriate uses artificially low thresholds to
find duplicate code.&lt;/p&gt;
&lt;h3 id=&quot;inaccurate-mass-calculation&quot; tabindex=&quot;-1&quot;&gt;Inaccurate mass calculation &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#inaccurate-mass-calculation&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are cases where the duplicate mass reported by Code Climate is not
accurate. For example, given the following code:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;<span class="highlight">&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;anotherFunction&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;value: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;value&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;nada&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;total: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
<span class="highlight2">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token comment&quot;&gt;// output error if value equal to 5</span>&lt;/span&gt;
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;total: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// log if less than 5&lt;/span&gt;
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code Climate reports the following mass values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The complete function &lt;code&gt;anotherFunction&lt;/code&gt; (highlighted in yellow) is reported to
have a mass of 58 (seems correct).&lt;/li&gt;
&lt;li&gt;Part of the function &lt;code&gt;function1&lt;/code&gt; (the &lt;code&gt;for&lt;/code&gt; loop highlighted in blue) is
reported to have a mass of 10 (definitely incorrect).&lt;/li&gt;
&lt;li&gt;Part of the function &lt;code&gt;function2&lt;/code&gt; (the &lt;code&gt;if&lt;/code&gt; statement highlighted in yellow),
which is a subset of the &lt;code&gt;function1&lt;/code&gt; &lt;code&gt;for&lt;/code&gt; loop, is reported to have a mass of
19 (seems correct).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This disparity makes it challenging to impossible to trust the results, and
certainly impossible to accurately tune the mass threshold to get the desired
results. Testing has confirmed these mass values are being used to filter
results and are not just a report display error.&lt;/p&gt;
&lt;h3 id=&quot;adjacent-duplicates-are-not-grouped&quot; tabindex=&quot;-1&quot;&gt;Adjacent duplicates are not grouped &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#adjacent-duplicates-are-not-grouped&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As noted previously, the code duplication algorithm looks at the tree of nodes
in the AST representation of the code. This means that if adjacent lines of code
are duplicates, but do not have a common parent node that includes only the
duplicate code, they are reported separately. For example, given the following
code:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
<span class="highlight">    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;total: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token comment&quot;&gt;// output error if value equal to 5</span>&lt;/span&gt;
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
<span class="highlight">    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;total: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token comment&quot;&gt;// log if less than 5</span>&lt;/span&gt;
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code Climate reports two sets of duplicates: the individual &lt;code&gt;console.log&lt;/code&gt; lines
as one (highlighted in yellow), and the complete &lt;code&gt;for&lt;/code&gt; loop as the other
(highlighted in blue).&lt;/p&gt;
&lt;p&gt;In addition, depending on the code in each adjacent duplicate group, the mass of
the combined code may be reportable as a duplicate, but the mass of each
individual line may not be, in which case the duplication is not reported at
all. This is illustrated in the following example, where five lines in each
function are exact duplicates, but &lt;code&gt;function2&lt;/code&gt; has an additional &lt;code&gt;console.log&lt;/code&gt;
line at the start.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://somewhere.test/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;x&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Fetching result for:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;result:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Checking ID: &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://somewhere.test/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;x&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Fetching result for:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;result:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Per Code Climate&#39;s algorithm, each of the five duplicate lines has a mass 12 -
16, with a total mass of 69. With the
&lt;a href=&quot;https://docs.codeclimate.com/docs/default-analysis-configuration#per-language-mass-threshold-defaults&quot;&gt;default JavaScript mass threshold of 45&lt;/a&gt;,
Code Climate reports no duplicate code, even though the collection of adjacent
lines together exceeds that threshold. The mass threshold would need to be
reduced to 12 to catch the duplication in individual lines, and then Code
Climate reports duplicates for each of the five lines separately.&lt;/p&gt;
&lt;h3 id=&quot;unidentified-duplicates&quot; tabindex=&quot;-1&quot;&gt;Unidentified duplicates &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#unidentified-duplicates&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are cases where the Code Climate duplication algorithm simply misses
duplicates. For example, given the following code, which has the same function
copied three times, each with the function/variables renamed and different
comments:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; badWords &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;badword1&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;badword2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;badword3&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

<span class="highlight">&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Redact bad words&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

<span class="highlight">&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;something&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code Climate reports the two highlighted functions (the first and third) as
duplicates, but the middle function is not identified as a duplicate. It&#39;s
possible the comment in the middle is impacting the results, but there are other
examples where this is not the case (for example,
&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#adjacent-duplicates-are-not-grouped&quot;&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&quot;noisy-similar-code-results&quot; tabindex=&quot;-1&quot;&gt;Noisy similar code results &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#noisy-similar-code-results&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Given the definition of &lt;em&gt;similar code&lt;/em&gt;, Code Climate frequently reports code
that is generally similar, and unlikely to be refactored. For example, given the
following code:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;total: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
        &lt;span class=&quot;token comment&quot;&gt;// output error if value equal to 5&lt;/span&gt;
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;function2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;total: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// log if less than 5&lt;/span&gt;
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;error&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code Climate reports the four highlighted &lt;code&gt;if&lt;/code&gt; statements as the same &lt;em&gt;similar
code&lt;/em&gt;. In this case it&#39;s unlikely that anyone would refactor the &lt;code&gt;if&lt;/code&gt; statements
to reduce duplication, especially the different comparison operators, so the
similar code results can be more noisy than useful.&lt;/p&gt;
&lt;h2 id=&quot;alternate-tool-options&quot; tabindex=&quot;-1&quot;&gt;Alternate tool options &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#alternate-tool-options&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Give the Code Climate issues previously discussed, a search was done looking for
alternative tools. The criteria for the tools were left as vague as practical:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tools needed to support most, if not all, of the languages that Code Climate
currently supports (&lt;em&gt;most&lt;/em&gt; was left undefined, but at least
JavaScript/TypeScript and Go, since that&#39;s what I write).&lt;/li&gt;
&lt;li&gt;Tools should detect &lt;em&gt;duplicate code&lt;/em&gt;, where &lt;em&gt;duplicate code&lt;/em&gt; should exclude
spacing, comments, and variable names/values to the greatest degree practical.
This terminology is different than Code Climate uses, where some of these
cases would constitute &lt;em&gt;similar code&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The tools had to be &lt;em&gt;actively maintained&lt;/em&gt;. That was also a little vague, but
to be &lt;em&gt;actively maintained&lt;/em&gt; a tool should have a release in the last year
(ideally in the last 3 months), commits in the last 3 months, and responses to
at least some new project issues.&lt;/li&gt;
&lt;li&gt;The tools must output results in some structured format that can be converted
to a GitLab Code Quality report.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some investigation led to two primary candidate tools warranting further
investigation, PMD Copy/Paste Detector (CPD) and jscpd. Both of these tools take
a different approach from Code Climate and use variations of the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm&quot;&gt;Rabin-Karp algorithm&lt;/a&gt;
for detecting duplicate code.&lt;/p&gt;
&lt;h3 id=&quot;pmd-copypaste-detector-(cpd)&quot; tabindex=&quot;-1&quot;&gt;PMD Copy/Paste Detector (CPD) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#pmd-copypaste-detector-(cpd)&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;PMD is primarily a static analysis tool for Java and Apex (and is itself a
Java-based tool), although provides some support for 16 other languages. For
comparison, it includes 273 rules for Java, but only 17 rules for JavaScript.
Included with it is the
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd.html&quot;&gt;PMD Copy/Paste Detector (CPD)&lt;/a&gt;,
which checks for duplicate code. PDM CPD supports
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd.html#supported-languages&quot;&gt;31 languages&lt;/a&gt;,
including all of the languages supported by Code Climate.&lt;/p&gt;
&lt;h3 id=&quot;jscpd&quot; tabindex=&quot;-1&quot;&gt;jscpd &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#jscpd&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/jscpd&quot;&gt;jscpd&lt;/a&gt; is a JavaScript-based tool to check
for duplicate code. It supports over 150 languages, including all of the
languages supported by Code Climate.&lt;/p&gt;
&lt;h3 id=&quot;comparison&quot; tabindex=&quot;-1&quot;&gt;Comparison &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#comparison&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In general, both tools are capable and produced similar results. Overall, PMD
CPD produced more comprehensive results, as illustrated in the following
examples. There were three primary areas where jscpd had limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;By design, jscpd only compares two sections of code, so for cases of 3 or more
duplications they&#39;re reported in pairs. With this, it&#39;s not as clear that
they&#39;re duplicates of the same code. Alternatively, PMD CPD reports all of the
duplications together in one group.&lt;/li&gt;
&lt;li&gt;There were some cases where jscpd reported multiple duplicates for the same
code where one was a subset of the other. This was more pronounced at lower
minimum token thresholds, which was used for many tests to test for the
greatest number of duplicates.&lt;/li&gt;
&lt;li&gt;The two machine-readable formats for jscpd did not include the token count for
the duplicates (the XML report doesn&#39;t include &lt;code&gt;tokens&lt;/code&gt;, and the JSON report
always erroneously reports &lt;code&gt;tokens: 0&lt;/code&gt;). Having this information is valuable
in adjusting the minimum token thresholds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following are a few examples for comparison. As with the previous examples,
these include simplified code samples for brevity, and where appropriate use
artificially low thresholds to find duplicate code. The specific code identified
as duplicate is highlighted in each case.&lt;/p&gt;
&lt;p&gt;The first is a JavaScript example with code duplicated 3 times, which finds
similar results, but illustrates that this is reported as different duplicates
by jscpd. It also shows where PMD CPD tended to include more code as part of the
duplicate.&lt;/p&gt;
&lt;div class=&quot;code-side-by-side&quot;&gt;&lt;div&gt;
&lt;p&gt;PMD CPD results&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; badWords &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;badword&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;badword2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&#39;badword3&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token comment&quot;&gt;// This is the same as the previous one</span>&lt;/span&gt;
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;something&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;jscpd results&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; badWords &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;badword&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;badword2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&#39;badword3&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value</span>&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token comment&quot;&gt;// This is the same as the previous one</span>&lt;/span&gt;
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input</span>&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;something&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; badWords &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;badword&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;badword2&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&#39;badword3&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value<span class="highlight2">&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value</span>&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// This is the same as the previous one&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;myFunction3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;something&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;badWords&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something<span class="highlight2">&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;***&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;</span>
<span class="highlight2">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight2">        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;something</span>&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The second example is in Go, showing similar results (both similar accuracy and
limitations).&lt;/p&gt;
&lt;div class=&quot;code-side-by-side&quot;&gt;&lt;div&gt;
&lt;p&gt;PMD CPD results&lt;/p&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt; main

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
 &lt;span class=&quot;token string&quot;&gt;&quot;fmt&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printOdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
<span class="highlight">    values &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; values &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;***&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printEven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
<span class="highlight">    values &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; values &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;***&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p&gt;jscpd results&lt;/p&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt; main

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
 &lt;span class=&quot;token string&quot;&gt;&quot;fmt&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printOdd&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    values &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; values &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;***&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printEven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    values &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; values &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;***&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;</span>
<span class="highlight">            fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;</span>
<span class="highlight">        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>
<span class="highlight">&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;</span>&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Given all of this data, PMD CPD was chosen for the solution.&lt;/p&gt;
&lt;h2 id=&quot;the-gitlab-pmd-cpd-solution&quot; tabindex=&quot;-1&quot;&gt;The GitLab PMD CPD solution &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#the-gitlab-pmd-cpd-solution&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This led to the creation of the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd&quot;&gt;GitLab PMD CPD&lt;/a&gt; project,
which includes a container image to run the PMD CPD analysis, and tools to
convert the results to a GitLab Code Quality report.&lt;/p&gt;
&lt;h3 id=&quot;conversion-to-gitlab-code-quality-report-format&quot; tabindex=&quot;-1&quot;&gt;Conversion to GitLab Code Quality report format &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#conversion-to-gitlab-code-quality-report-format&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Unfortunately CPM CPD can&#39;t accept any kind of template for the output, and from
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd_report_formats.html&quot;&gt;the available options&lt;/a&gt;
XML is the only structured format. So, a simple Go-based CLI application was
created to post-process the XML output, converting it into GitLab Code Quality
report formatted JSON. The container image used to run PMD CPD is based on a
Java image, and this allowed a binary that could easily be added without
requiring extra dependencies.&lt;/p&gt;
&lt;p&gt;The GitLab documentation provides details on the required schema for
implementing a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool&quot;&gt;custom code quality report&lt;/a&gt;.
This includes the minimum values used by GitLab for displaying the results in
the merge request widget or the pipeline view - description, check name,
fingerprint, severity, and start line. A review of the GitLab Code Quality and
PMD CPD reports showed some other useful data, which was included. An example
with the complete final schema is:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;categories&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Duplication&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;check_name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;duplicate-code&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;    const a = [1, 3, 5];&#92;n    console.log(`total: ${a.length}`);&#92;n    for (let i = 0; i &amp;lt; a.length; i++) {&#92;n        if (a[i] &amp;lt; 5) {&#92;n            console.log(a[i]);&#92;n        }&#92;n        // output error if value equal to 5&#92;n        if (a[i] === 5) {&#92;n            console.log(&#39;error&#39;);&#92;n        }&#92;n    }&#92;n};&#92;n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;group&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;tokens&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;68&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Duplicate code found in 2 locations (Duplication 1). Consider refactoring.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;engine_name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pmd-cpd&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;severity&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;major&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;issue&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;fingerprint&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d0bf6d31894d6482691775a4f031e680dff4a8ab02d2a3d5be036dfdb920002e-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;location&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;internal/testdata/exampleproject/file5.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;positions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;begin&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;23&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;other_locations&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;internal/testdata/exampleproject/file5.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;positions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;begin&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token property&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;27&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few changes were made from the original GitLab Code Quality report:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;content.body&lt;/code&gt; value includes the duplicate code, whereas the Code Climate
report gives a long description on DRY-ness and why duplicate code can be a
problem (the same for all duplicates other than the actual mass value). The
specific code proved more valuable.&lt;/li&gt;
&lt;li&gt;The total &lt;code&gt;tokens&lt;/code&gt; as well as the &lt;code&gt;group&lt;/code&gt; for the duplicates were added to the
&lt;code&gt;content&lt;/code&gt; object. Because &lt;code&gt;tokens&lt;/code&gt; is included, there is no
&lt;code&gt;remediation_points&lt;/code&gt; field which the Code Climate report provides (although
I&#39;m still not sure the value of trying to &lt;em&gt;score&lt;/em&gt; the badness of code quality
issues). The &lt;code&gt;group&lt;/code&gt; is unique to a duplication, and added to differentiate
results because each duplicate location is included in the report for each
occurrence (as &lt;code&gt;location&lt;/code&gt; and as &lt;code&gt;other_location&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;For simplicity, all &lt;code&gt;severity&lt;/code&gt; values are &lt;code&gt;major&lt;/code&gt;, which seemed consistent
with Code Climate results for token thresholds likely to be used in
production. The actual Code Climate results, as with the mass threshold
variances discussed previously, were an inconsistent mix of &lt;code&gt;major&lt;/code&gt;/&lt;code&gt;minor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;fingerprint&lt;/code&gt; value is a SHA256 hash instead of MD5 to avoid any possible
collisions. It is also suffixed with the occurrence number of that duplicate.
This creates a unique &lt;code&gt;fingerprint&lt;/code&gt; value for each instance of each duplicate,
which causes all occurrences to be displayed in the GitLab merge request
widget. GitLab displays only the last occurrence of each unique &lt;code&gt;fingerprint&lt;/code&gt;,
so with the Code Climate report only the last occurrence of each duplication
is shown and the report must be opened to find the location of the other
occurrences. This implementation causes all occurrences to be shown in the
merge request widget with links to the specific code for each.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The following example shows the difference in how the results are displayed in
GitLab.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/pmd-cpd-gitlab.webp&quot; width=&quot;748&quot; height=&quot;475&quot; alt=&quot;Comparison of results as displayed in the GitLab Code Quality widget&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;p&gt;The first three are the results from the original Code Climate report (which
show only the last occurrence of each duplicate, and ungrouped duplicates). The
remaining five are from the converted PMD CPD results (which show all
occurrences of each duplicate). These results are from the previous examples
&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#inaccurate-mass-calculation&quot;&gt;here&lt;/a&gt; (&lt;code&gt;file4.js&lt;/code&gt;) and
&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#adjacent-duplicates-are-not-grouped&quot;&gt;here&lt;/a&gt; (&lt;code&gt;file5.js&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;running-analyses&quot; tabindex=&quot;-1&quot;&gt;Running analyses &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#running-analyses&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The GitLab PMD CPD container images can be found at
&lt;code&gt;registry.gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd&lt;/code&gt;. The image contains all the
tools to run the analysis and report conversion, and a
&lt;code&gt;/gitlab-pmd-cpd/pmd-cpd.sh&lt;/code&gt; script as a wrapper to set default values and run
all aspects of the analysis and conversion. The script can be customized with
the following environment variables. Note that the analysis requires specifying
a language, so must be repeated for each applicable language (although that&#39;s
easy to automate, as shown in the subsequent GitLab CI example).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PMDCPD_DIR_FILES&lt;/code&gt;: The directory or files to scan for duplicate code
(default: &lt;code&gt;--dir=.&lt;/code&gt;). This must be one of the options &lt;code&gt;--dir&lt;/code&gt;, &lt;code&gt;--file-list&lt;/code&gt;
or &lt;code&gt;--uri&lt;/code&gt; with the appropriate value (for example &lt;code&gt;--file-list=files.txt&lt;/code&gt;).
Some language-specific examples are shown in the subsequent GitLab CI example.
See the
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd.html#cli-options-reference&quot;&gt;PMD CPD docs&lt;/a&gt;
for additional details.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PMDCPD_MIN_TOKENS&lt;/code&gt;: The minimum number of tokens to consider a code fragment
as duplicate (default: &lt;code&gt;100&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PMDCPD_LANGUAGE&lt;/code&gt;: The language to use for duplicate code detection (default:
&lt;code&gt;ecmascript&lt;/code&gt;). The complete list of supported languages can be found
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd.html#supported-languages&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PMDCPD_RESULTS&lt;/code&gt;: The name of the PMD CPD results file (default:
&lt;code&gt;pmd-cpd-results.xml&lt;/code&gt;). Note that the file extension must be &lt;code&gt;.xml&lt;/code&gt; for the
conversion to GitLab Code Quality report format to work properly, and that the
resulting JSON file uses the same base name (for example
&lt;code&gt;pmd-cpd-results.json&lt;/code&gt; for the default name).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PMDCPD_CLI_ARGS&lt;/code&gt;: Any additional PMD CPD command line arguments (default:
none). See the
&lt;a href=&quot;https://pmd.github.io/pmd/pmd_userdocs_cpd.html#cli-usage&quot;&gt;PMD CPD docs&lt;/a&gt; for
additional details.
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;--exclude &amp;lt;path&amp;gt;&lt;/code&gt; option can be used to exclude files from the scan.
This option can be used multiple times. &lt;strong&gt;Note this does not allow globs.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Note that the &lt;code&gt;--no-fail-on-violation&lt;/code&gt;, &lt;code&gt;--skip-lexical-errors=true&lt;/code&gt;, and
&lt;code&gt;--format=&amp;quot;xml&amp;quot;&lt;/code&gt; values are fixed in the script.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PMD_JAVA_OPTS&lt;/code&gt;: This is used by PMD to set various JVM options, including the
heap size. For example, &lt;code&gt;PMD_JAVA_OPTS=-Xmx1024m&lt;/code&gt; to set the maximum heap size
size to 1 GB.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not that in an effort to avoid a fight with Oracle, ECMAScript is used instead
of
&lt;a href=&quot;https://www.theregister.com/2024/09/17/oracle_urged_to_surrender_javascript_trademark/&quot;&gt;JavaScript ®&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;issues-with-shebang-in-javascripttypescript&quot; tabindex=&quot;-1&quot;&gt;Issues with shebang in JavaScript/TypeScript &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#issues-with-shebang-in-javascripttypescript&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;When running the CPD analysis, PMD parses each file. Since it is set to ignore
lexical errors, files that fail to parse are ignored (otherwise the analysis
would fail). One issue that was noted with the parser is that it considers a
shebang in JavaScript and TypeScript files to be invalid syntax. This is
typically seen in executable &lt;code&gt;bin&lt;/code&gt; files in Node.js projects. There is an
&lt;a href=&quot;https://github.com/pmd/pmd/issues/1756&quot;&gt;open PMD issue&lt;/a&gt; to resolve this, but
until then the &lt;code&gt;pmd-cpd.sh&lt;/code&gt; script simply removes the shebang lines before
running the analysis. When running in GitLab CI in a container with an ephemeral
copy of the repository only for this analysis this is not an issue. If the
analysis is run locally, note that it changes these files and does not revert
those changes itself.&lt;/p&gt;
&lt;h2 id=&quot;gitlab-ci-jobs&quot; tabindex=&quot;-1&quot;&gt;GitLab CI jobs &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-code-quality-duplication-analysis-with-pmd-cpd/#gitlab-ci-jobs&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The following are example GitLab CI jobs to check for duplication in JavaScript,
TypeScript, and Go.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;.duplication_base&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pmd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cpd&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; test
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_MIN_TOKENS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS_BASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pmd-cpd-results&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;${PMDCPD_RESULTS_BASE}.xml&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pmd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cpd/pmd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cpd.sh
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;PMDCPD_RESULTS_BASE&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.*
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;codequality&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;PMDCPD_RESULTS_BASE&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.json

&lt;span class=&quot;token key atrule&quot;&gt;duplication_go&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .duplication_base
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_LANGUAGE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;go&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_DIR_FILES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;--file-list=go_files.txt&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS_BASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pmd-cpd-results-go&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/*.go&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; find . &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;type f &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;name &quot;&lt;span class=&quot;token important&quot;&gt;*.go&quot;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;name &quot;&lt;span class=&quot;token important&quot;&gt;*_test.go&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; go_files.txt

&lt;span class=&quot;token key atrule&quot;&gt;duplication_js&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .duplication_base
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_LANGUAGE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;ecmascript&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_CLI_ARGS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;--exclude ./tests/&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS_BASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pmd-cpd-results-js&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/*.js&#39;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;duplication_ts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .duplication_base
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_LANGUAGE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;typescript&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_CLI_ARGS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;--exclude ./tests/&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS_BASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pmd-cpd-results-ts&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/*.ts&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;.duplication_base&lt;/code&gt; template sets common defaults, including the &lt;code&gt;image&lt;/code&gt;,
default &lt;code&gt;variables&lt;/code&gt;, &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;artifacts&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;As noted previously, the language to check for duplicates must be specified,
so there&#39;s a separate job for each language, with &lt;code&gt;rules&lt;/code&gt; to ensure the
applicable jobs run based on file extensions in the repository.&lt;/li&gt;
&lt;li&gt;One noteworthy item to be aware of is the different uses of the
&lt;code&gt;PMDCPD_DIR_FILES&lt;/code&gt; variable.
&lt;ul&gt;
&lt;li&gt;For JavaScript/TypeScript, the default value of &lt;code&gt;--dir=.&lt;/code&gt; is used. This
assume a model where test files, for example, are kept in a dedicated
&lt;code&gt;tests/&lt;/code&gt; directory, which is excluded with the &lt;code&gt;PMDCPD_CLI_ARGS&lt;/code&gt; variable.
If test files are kept with the code, the model used here for Go files
should be used.&lt;/li&gt;
&lt;li&gt;For Go, tests files are kept with the applicable packages. Since the
&lt;code&gt;--exclude &amp;lt;path&amp;gt;&lt;/code&gt; option doesn&#39;t accept globs, it can&#39;t be used to ignore
all &lt;code&gt;*_test.go&lt;/code&gt; files. So, in &lt;code&gt;before_script&lt;/code&gt; a file is created that lists
all &lt;code&gt;*.go&lt;/code&gt; files except the &lt;code&gt;*_test.go&lt;/code&gt; files. Doing this in &lt;code&gt;before_script&lt;/code&gt;
avoids changing the template &lt;code&gt;script&lt;/code&gt;. The file list is then used for the
duplication analysis, specified with
&lt;code&gt;PMDCPD_DIR_FILES: &#39;--file-list=go_files.txt&#39;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These jobs could be easily extended for other required languages.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>GitLab CI Pipeline for Go Projects</title>

    <link href="https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/" />
    <updated>2024-01-28T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/</id>
    <content type="html">&lt;p&gt;After recently learning Go, and about to start my first project, I started
putting together a GitLab CI pipeline for Go projects. After scouring the
Internet, I found mostly old posts on the subject. The posted pipelines, as well
as numerous actual Go project pipelines, were missing jobs in my set of goals
for a CI pipeline. So I built this GitLab CI pipeline for Go projects.&lt;/p&gt;
&lt;h2 id=&quot;pipeline-goals&quot; tabindex=&quot;-1&quot;&gt;Pipeline goals &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#pipeline-goals&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This adventure started with my first Go project, and wanting, but not finding a
pipeline that met the goals that I have for just about any pipeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The pipeline should include all checks required to confirm the software is in
a deployable state, and should generate any reporting required when deploying
the software.&lt;/li&gt;
&lt;li&gt;The pipeline jobs should use &lt;code&gt;rules&lt;/code&gt; logic to include or exclude jobs or
settings where an expectation is implied. For example, it should analyze
dependencies if the project includes dependencies (if a &lt;code&gt;go.sum&lt;/code&gt; file exists),
or deploy binaries if binaries are built, but should exclude those jobs where
they are not applicable.&lt;/li&gt;
&lt;li&gt;The pipeline should allow common configuration options through &lt;code&gt;variables&lt;/code&gt;
(globally if they effect multiple jobs to avoid duplication).&lt;/li&gt;
&lt;li&gt;The pipeline should automate deployment when the project should be deployed to
ensure consistency.&lt;/li&gt;
&lt;li&gt;The pipeline jobs should provide
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html&quot;&gt;&lt;code&gt;reports&lt;/code&gt;&lt;/a&gt; to
leverage GitLab capabilities, where available. For example, &lt;code&gt;code_quality&lt;/code&gt;,
&lt;code&gt;junit&lt;/code&gt;, and &lt;code&gt;coverage&lt;/code&gt; &lt;code&gt;reports&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The pipeline should be optimized to execute as many jobs as possible in
parallel with clearly defined job dependencies, and should minimize repeating
the same tasks in favor of performing them once and passing the artifacts as
required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pipeline detailed here does not include common jobs that fall under the
previously defined goals, but are already available generically in
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;my templates&lt;/a&gt;, which
includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code quality analysis for common languages (excluding Go) that are typically
included in a GitLab project. For example linting markdown, linting YAML,
linting shell scripts, counting code by language.&lt;/li&gt;
&lt;li&gt;Code security analysis including Static Application Security Testing (SAST)
and secret detection.&lt;/li&gt;
&lt;li&gt;Supply chain security analysis including several flavors of dependency
vulnerability scanning and &lt;a href=&quot;https://socket.dev/&quot;&gt;Socket&lt;/a&gt; dependency analysis.&lt;/li&gt;
&lt;li&gt;Jobs to build, test, and deploy container images, which are not unique to Go.&lt;/li&gt;
&lt;li&gt;Jobs to automatically create a GitLab release.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;gitlab-pipeline-for-go-projects&quot; tabindex=&quot;-1&quot;&gt;GitLab pipeline for Go projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#gitlab-pipeline-for-go-projects&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The complete CI pipeline for Go projects is shown below, ordered by execution
sequence with job dependencies shown. The first is for a Go project with
dependencies.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/go-pipeline-deps-dag.webp&quot; width=&quot;1107&quot; height=&quot;548&quot; alt=&quot;GitLab CI pipeline for Go project with dependencies&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;p&gt;The second is the same pipeline showing how it adapts if the project has no
dependencies.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/go-pipeline-nodeps-dag.webp&quot; width=&quot;668&quot; height=&quot;481&quot; alt=&quot;GitLab CI pipeline for Go project with no dependencies&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;p&gt;The details of all jobs are discussed in the subsequent sections, which assumes
a working knowledge of both Go and GitLab CI. The included CI jobs have some
simplifications for this post where specific aspects are not relevant for this
introduction. Each section also includes a link to the complete job in my
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;templates project&lt;/a&gt; for
reference.&lt;/p&gt;
&lt;h3 id=&quot;global-go-template&quot; tabindex=&quot;-1&quot;&gt;Global Go template &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#global-go-template&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To establish commonality across all of the jobs running Go code, a
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/templates/Go-Templates.gitlab-ci.yml&quot;&gt;&lt;code&gt;.go&lt;/code&gt;&lt;/a&gt;
template was created with several global settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define a common &lt;code&gt;image&lt;/code&gt; so jobs are all running on the same version of Go.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;GOPATH&lt;/code&gt; to &lt;code&gt;$CI_PROJECT_DIR/.go&lt;/code&gt; so that it&#39;s within the project
directory. This allows saving its contents as artifacts and/or cache (which
must be within the project directory), and establishes a common directory for
these jobs.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;GO_BIN_DIR&lt;/code&gt; to a common location where all binaries are saved to and
retrieved from.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;.go&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; golang&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;1.21.6&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;GOPATH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_PROJECT_DIR/.go
    &lt;span class=&quot;token key atrule&quot;&gt;GO_BIN_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bin&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some jobs require changes to these settings, and those are specifically
identified below.&lt;/p&gt;
&lt;p&gt;Note: one impact to resetting &lt;code&gt;GOPATH&lt;/code&gt; is that becomes the location for any
&lt;code&gt;go install&lt;/code&gt;ed modules, which do install nominally, but that directory is not
included in &lt;code&gt;PATH&lt;/code&gt;. So, those modules either need to be executed from
&lt;code&gt;$GOPATH/bin&lt;/code&gt; or &lt;code&gt;$GOPATH/bin&lt;/code&gt; needs to be added to &lt;code&gt;PATH&lt;/code&gt; before executing.&lt;/p&gt;
&lt;h3 id=&quot;download-dependencies&quot; tabindex=&quot;-1&quot;&gt;Download dependencies &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#download-dependencies&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;It&#39;s a common expectation today for applications to have external dependencies,
and sometimes there are many, either direct or transitive. These dependencies
are required in multiple subsequent jobs, so if there are any (that is, if a
&lt;code&gt;go.sum&lt;/code&gt; file exists), then they&#39;re downloaded first in the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Mod-Download.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_mod_download&lt;/code&gt;&lt;/a&gt;
job and saved as artifacts for use in those subsequent jobs. This not only saves
time in subsequent jobs, but more importantly ensures that all jobs are using
the same versions. They&#39;re also saved in a project-wide cache to reduce download
times on subsequent pipelines.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_mod_download&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/go.sum&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Required since this is set as GOPATH&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; mkdir &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;p .go
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go mod download
  &lt;span class=&quot;token key atrule&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;CI_PROJECT_NAME&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;go
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go/pkg/mod/
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go/pkg/mod/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This assumes that the project is not
&lt;a href=&quot;https://go.dev/ref/mod#vendoring&quot;&gt;vendoring dependencies&lt;/a&gt;. Despite the fact the
Go includes a command to specifically do it (&lt;code&gt;go mod vendor&lt;/code&gt;), I believe
vendoring promotes bad configuration management practices, potential licensing
concerns, and is not recommended so it is not accounted for.&lt;/p&gt;
&lt;h3 id=&quot;linting&quot; tabindex=&quot;-1&quot;&gt;Linting &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#linting&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Linting is accomplished via the &lt;code&gt;lint_go&lt;/code&gt; job using
&lt;a href=&quot;https://github.com/golangci/golangci-lint&quot;&gt;&lt;code&gt;golangci-lint&lt;/code&gt;&lt;/a&gt;, which integrates
dozens of Go linting apps, running them with concurrent execution (of course,
it&#39;s a Go app). A YAML file (&lt;code&gt;.golangci.yml&lt;/code&gt;) can be used to configure the
enabled linters, settings for each linter, etc. The job is setup to use the
&lt;code&gt;.golangci.yml&lt;/code&gt; in the project root if one exists, otherwise a default file is
retrieved from a URL (which can be overridden via a variable, as shown in the
example). The results are saved as a GitLab Code Quality JSON report uploaded to
GitLab so they&#39;re available in the Code Quality merge request widget. A summary
is also output to the job log.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lint_go&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; golangci/golangci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lint&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;v1.55.2&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;CONFIG_FILE_LINK&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/config&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;files/&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;/raw/10.1.1/Linters/.golangci.yml
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/*.go&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; apk add jq
    &lt;span class=&quot;token comment&quot;&gt;# Use default .golangci.yml file if one does not exist&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;f .golangci.yml &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;; then (wget $CONFIG_FILE_LINK) fi
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Write the code coverage report to gl-code-quality-report.json&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# and print linting issues to stdout in the format: path/to/file:line description&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      golangci-lint run --out-format code-climate $LINT_GO_CLI_ARGS |
      tee gl-code-quality-report.json |
      jq -r &#39;.[] | &quot;&#92;(.location.path):&#92;(.location.lines.begin) &#92;(.description)&quot;&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;codequality&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gl&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;quality&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report.json
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; gl&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;quality&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report.json
  &lt;span class=&quot;token key atrule&quot;&gt;allow_failure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;duplication&quot; tabindex=&quot;-1&quot;&gt;Duplication &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#duplication&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Duplication.gitlab-ci.yml&quot;&gt;&lt;code&gt;duplication_go&lt;/code&gt;&lt;/a&gt;
job looks for duplicate code across Go files. The default duplication threshold
is 50 tokens, but can be adjusted as desired via a variable. The job also
defaults to checking all &lt;code&gt;*.go&lt;/code&gt; files, but excluding all &lt;code&gt;*_test.go&lt;/code&gt; files,
since test code tends to have a lot of duplicate boilerplate code. This is done
by creating a list of files to check, which is done in &lt;code&gt;before_script&lt;/code&gt; so it can
be easily overridden without impacting other job logic, and that listing is
passed as a variable. The results from this job are also saved as a GitLab Code
Quality JSON report uploaded to GitLab so they&#39;re available in the Code Quality
merge request widget. Full details on this job can be found in the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/blob/main/README.md&quot;&gt;GitLab PMD CPD project&lt;/a&gt;,
which are beyond the scope of this post.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;duplication_go&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pmd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cpd&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_MIN_TOKENS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_LANGUAGE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;go&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_DIR_FILES&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;--file-list=go_files.txt&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS_BASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pmd-cpd-results-go&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PMDCPD_RESULTS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;${PMDCPD_RESULTS_BASE}.xml&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/*.go&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; find . &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;type f &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;name &quot;&lt;span class=&quot;token important&quot;&gt;*.go&quot;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;name &quot;&lt;span class=&quot;token important&quot;&gt;*_test.go&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; go_files.txt
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pmd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cpd/pmd&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cpd.sh
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;PMDCPD_RESULTS_BASE&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.*
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;codequality&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;PMDCPD_RESULTS_BASE&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;.json&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;build&quot; tabindex=&quot;-1&quot;&gt;Build &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#build&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are two build jobs, both optional, and either one or both can be included
via a variable. These jobs specifically build binaries, so if the build is only
in a &lt;code&gt;Dockerfile&lt;/code&gt;, for example, they may not be required. If either job is
included, the job needs are set to the &lt;code&gt;go_mod_download&lt;/code&gt; job to use those
downloaded dependencies.&lt;/p&gt;
&lt;h4 id=&quot;build-current-operating-system-and-architecture&quot; tabindex=&quot;-1&quot;&gt;Build current operating system and architecture &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#build-current-operating-system-and-architecture&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Build.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_build&lt;/code&gt;&lt;/a&gt;
job performs a build with only the current OS/architecture (Linux/amd64 in the
given container) and saves the binary as an artifact, with the binary name and
&lt;code&gt;main&lt;/code&gt; package directory specified via variables.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $GO_BUILD_CURRENT
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;GO_BUILD_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_PROJECT_NAME
    &lt;span class=&quot;token key atrule&quot;&gt;GO_MAIN_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go build $GO_BUILD_ARGS &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;o ./$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GO_BIN_DIR&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GO_BUILD_NAME&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; $GO_MAIN_DIR
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GO_BIN_DIR&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GO_BUILD_NAME&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;*&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;build-multiple-operating-systems-and-architectures&quot; tabindex=&quot;-1&quot;&gt;Build multiple operating systems and architectures &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#build-multiple-operating-systems-and-architectures&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Build.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_build_multi&lt;/code&gt;&lt;/a&gt;
job also performs a build, but in a parallel matrix job for a variety of
OS/architecture combinations, all of which are saved as artifacts with the
OS/architecture appended to the binary name. If the OS is Windows, the job adds
the &lt;code&gt;.exe&lt;/code&gt; extension to the binary name. This can be overridden as necessary.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_build_multi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $GO_BUILD_MULTI
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;GO_BUILD_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_PROJECT_NAME
    &lt;span class=&quot;token key atrule&quot;&gt;GO_MAIN_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;parallel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;matrix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;TARGET_OS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;darwin&#39;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;TARGET_ARCH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;amd64&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;arm64&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;TARGET_OS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;linux&#39;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;TARGET_ARCH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;amd64&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;arm64&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;TARGET_OS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;windows&#39;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;TARGET_ARCH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;386&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;amd64&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; if &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; $TARGET_OS == &#39;windows&#39; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;; then export EXTENSION=&#39;.exe&#39;; fi
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      env GOOS=${TARGET_OS} GOARCH=${TARGET_ARCH}
      go build $GO_BUILD_ARGS -o ./${GO_BIN_DIR}/${GO_BUILD_NAME}_${TARGET_OS}_${TARGET_ARCH}${EXTENSION} $GO_MAIN_DIR&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GO_BIN_DIR&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;/$&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;GO_BUILD_NAME&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;*&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parallel matrix runs all jobs in parallel for efficiency.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/go-pipeline-deps-dag-multi.webp&quot; width=&quot;1250&quot; height=&quot;550&quot; alt=&quot;GitLab CI pipeline for Go project with dependencies&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;h3 id=&quot;unit-and-integration-testing&quot; tabindex=&quot;-1&quot;&gt;Unit and integration testing &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#unit-and-integration-testing&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are two test jobs, both of which run in a custom
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test&quot;&gt;container image&lt;/a&gt;.
This image is based on the &lt;code&gt;golang&lt;/code&gt; image, with two sets of pre-installed
dependencies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tools to generate JUnit formatted test results and Cobertura coverage reports.&lt;/li&gt;
&lt;li&gt;OS dependencies including &lt;code&gt;gcc&lt;/code&gt; that are required to run with &lt;code&gt;-race&lt;/code&gt; , which
requires &lt;code&gt;Cgo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/templates/Go-Templates.gitlab-ci.yml&quot;&gt;&lt;code&gt;.go_test&lt;/code&gt;&lt;/a&gt;
template that specifies the version of this image (which also &lt;code&gt;extend&lt;/code&gt;s the
&lt;code&gt;.go&lt;/code&gt; template), and all test jobs &lt;code&gt;extend&lt;/code&gt; it rather than the &lt;code&gt;.go&lt;/code&gt; template.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;.go_test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/container&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;images/go&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;test&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;2.0.1
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As with the build jobs, if there are dependencies each test job &lt;code&gt;needs&lt;/code&gt; the
&lt;code&gt;go_mod_download&lt;/code&gt; job.&lt;/p&gt;
&lt;h4 id=&quot;tests-with-code-coverage&quot; tabindex=&quot;-1&quot;&gt;Tests with code coverage &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#tests-with-code-coverage&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Test.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_test&lt;/code&gt;&lt;/a&gt;
job runs all tests, with code coverage, and processes the results generating
several additional reports for ingestion by GitLab and reviewing pipeline
results.&lt;/p&gt;
&lt;p&gt;All tests are run, generating coverage information for all packages. Well, the
goal is all packages, but there&#39;s an
&lt;a href=&quot;https://github.com/golang/go/issues/24570&quot;&gt;open issue&lt;/a&gt; on Go not including
coverage data for packages with no tests. The workaround used here is to use the
&lt;code&gt;-coverpkg&lt;/code&gt; argument to specify package to include in the coverage data. This is
specified through the variable &lt;code&gt;GO_COVER_PACKAGES&lt;/code&gt;. Ideally this could simply be
set to &lt;code&gt;./...&lt;/code&gt;, but this is one of the places where redirecting &lt;code&gt;GOPATH&lt;/code&gt; has an
impact. Since it is within the current working directory, then &lt;code&gt;./...&lt;/code&gt; includes
all the project dependencies. So, &lt;code&gt;GO_COVER_PACKAGES&lt;/code&gt; is specified by using
&lt;code&gt;go list ./...&lt;/code&gt;, which only includes packages in the current module, then
converted to a comma delimited list, which &lt;code&gt;-coverpkg&lt;/code&gt; requires. This is done in
&lt;code&gt;before_script&lt;/code&gt; so it can be easily overridden without impacting other test
logic.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# Get list of packages, replace newline with comma, and remove training comma.&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; export GO_COVER_PACKAGES=$(go list ./&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; tr &#39;&#92;n&#39; &#39;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&#39; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; sed &#39;s/&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;$//&#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even with this, there&#39;s still one quirk, as noted in the linked Go issue -
packages with no tests are not included in the coverage data. The best
workaround seems to be creating a test file with just the package declaration
(for example &lt;code&gt;package foo_test&lt;/code&gt;), then the coverage is included as zero percent
for all functions in that package (for &lt;code&gt;foo&lt;/code&gt; in this example). The fix for the
original Go issue is merged and scheduled for Go 1.22.&lt;/p&gt;
&lt;p&gt;When the tests are run, in addition to generating coverage data the test output
is also saved to a file (&lt;code&gt;tests.txt&lt;/code&gt;) that is used for reporting. Once
completed, the &lt;code&gt;go tool cover&lt;/code&gt; command is used to output a summary of coverage
information by function, and the total coverage is also collected by GitLab.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go test &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;coverpkg=&quot;$GO_COVER_PACKAGES&quot; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;coverprofile=coverage.out &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;covermode=count ./&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; tee `tests.txt`
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go tool cover &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;func coverage.out&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The various test summary reports are generated in &lt;code&gt;after_script&lt;/code&gt;, which ensures
that errors generating reports do not fail the job. This includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converting the coverage data to a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/test_coverage_visualization.html&quot;&gt;Cobertura coverage report&lt;/a&gt;,
ingested by GitLab. This allows coverage data to be visible in the merge
request diff view.&lt;/li&gt;
&lt;li&gt;Converting the coverage data to an HTML coverage report, which is saved as an
artifact.&lt;/li&gt;
&lt;li&gt;Converting the test output to a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/unit_test_reports.html&quot;&gt;JUnit report&lt;/a&gt;,
ingested by GitLab. This allows the test summary to be presented in a GitLab
merge request widget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This results in the following job configuration:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go_test
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Get list of packages, replace newline with comma, and remove training comma.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# If `./...` is used, coverage includes packages in GOPATH since set within&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# the cwd.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Done in before_script so it can be easily overridden.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; export GO_COVER_PACKAGES=$(go list ./&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; tr &#39;&#92;n&#39; &#39;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&#39; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; sed &#39;s/&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;$//&#39;)
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go test &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;coverpkg=&quot;$GO_COVER_PACKAGES&quot; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;coverprofile=coverage.out &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;covermode=count ./&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; tee tests.txt
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go tool cover &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;func coverage.out
  &lt;span class=&quot;token key atrule&quot;&gt;after_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Generate Cobertura coverage report for GitLab&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; gocover&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cobertura &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;by&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;files &amp;lt; coverage.out &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; coverage.xml
    &lt;span class=&quot;token comment&quot;&gt;# Generate HTML coverage report&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go tool cover &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;html coverage.out &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;o coverage.html
    &lt;span class=&quot;token comment&quot;&gt;# Generate JUnit report for GitLab&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;junit&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;report &amp;lt; tests.txt &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; junit.xml
  &lt;span class=&quot;token key atrule&quot;&gt;coverage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/total:&#92;s+&#92;(statements&#92;)&#92;s+&#92;d+.&#92;d+%/&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;expose_as&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Go Test Coverage Report&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; coverage.html
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;coverage_report&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;coverage_format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cobertura
        &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; coverage.xml
      &lt;span class=&quot;token key atrule&quot;&gt;junit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; junit.xml&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;test-race-conditions&quot; tabindex=&quot;-1&quot;&gt;Test race conditions &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#test-race-conditions&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Test-Race.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_test_race&lt;/code&gt;&lt;/a&gt;
job runs all tests with the &lt;code&gt;-race&lt;/code&gt; flag to check for race conditions. It is
configured with &lt;code&gt;CGO_ENABLED&lt;/code&gt;, as required to use &lt;code&gt;-race&lt;/code&gt;. No artifacts are
saved as they are all covered with the &lt;code&gt;go_test&lt;/code&gt; job.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_test_race&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go_test
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Cgo is required for -race.&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;CGO_ENABLED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go test ./&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;race&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If tests with &lt;code&gt;-race&lt;/code&gt; run slowly on gitlab.com runners, consider using more
capable runners. By observation (not testing), this job is CPU bound and appears
to scale roughly linearly with CPU (that is, doubling CPU cuts the execution
time in half), at least through the &lt;code&gt;large&lt;/code&gt; runners. See GitLab SaaS runner
details in
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html&quot;&gt;the docs&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;analysis-jobs&quot; tabindex=&quot;-1&quot;&gt;Analysis jobs &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#analysis-jobs&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are several application analysis jobs performed under the umbrella of
confirming the software is in a deployable state and reporting required when
deploying the software.&lt;/p&gt;
&lt;h4 id=&quot;missing-or-extraneous-dependencies&quot; tabindex=&quot;-1&quot;&gt;Missing or extraneous dependencies &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#missing-or-extraneous-dependencies&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Mod-Tidy.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_mod_tidy&lt;/code&gt;&lt;/a&gt;
job identifies whether the &lt;code&gt;go mod tidy&lt;/code&gt; command produces any changes,
functionally identifying any missing or extraneous dependencies in the project
which can, and should, be fixed with &lt;code&gt;go mod tidy&lt;/code&gt;. This job only runs if a
&lt;code&gt;go.sum&lt;/code&gt; file exists, and if there are dependencies &lt;code&gt;needs&lt;/code&gt; the
&lt;code&gt;go_mod_download&lt;/code&gt; job.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_mod_tidy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/go.sum&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      # Check if go mod tidy produces any changes
      if [ ! -f &quot;go.mod&quot; ]; then echo &quot;go.mod file not found&quot; &amp;amp;&amp;amp; exit 2; fi
      cp go.mod go.mod.before
      cp go.sum go.sum.before
      go mod tidy
      diff go.mod.before go.mod &amp;amp;&amp;amp; diff go.sum.before go.sum&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;dependency-updates&quot; tabindex=&quot;-1&quot;&gt;Dependency updates &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#dependency-updates&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Mod-Updates.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_mod_updates&lt;/code&gt;&lt;/a&gt;
job identifies any potential updates in direct dependencies. This goes a step
beyond the core Go command &lt;code&gt;go list -m -u&lt;/code&gt;, which identifies potential updates
in any dependencies - direct or transitive. Updates to transitive dependencies
can be out of the project&#39;s direct control for various reasons, so they are
ignored in this case to avoid unnecessary job failures. The job uses the
&lt;code&gt;go list -m -u&lt;/code&gt; JSON results to find direct dependency updates, and fail any are
found. This job only runs if a &lt;code&gt;go.sum&lt;/code&gt; file exists, and if there are
dependencies &lt;code&gt;needs&lt;/code&gt; the &lt;code&gt;go_mod_download&lt;/code&gt; job. It also allows failure since its
primary purpose is to identify the available dependency updates.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_mod_updates&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/go.sum&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;allow_failure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; apk add jq
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Waiting on https://github.com/golang/go/issues/40364 to allow listing&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# only direct dependencies so don&#39;t have to check JSON.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      # Check for available updates to any direct module dependencies
      UPDATES=$(go list -u -m -json all | &#92;
        jq -c -r &#39;select(.Update != null and .Indirect == null) | &quot;&#92;(.Path) &#92;(.Version) [&#92;(.Update.Version)]&quot;&#39;)
      printf &quot;$UPDATES&#92;n&quot;
      if [ $(echo $UPDATES | wc -w) != &quot;0&quot; ]; then exit 1; fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note there is an &lt;a href=&quot;https://github.com/golang/go/issues/40364&quot;&gt;open issue&lt;/a&gt; to
allow &lt;code&gt;go list&lt;/code&gt; to show only updates to direct dependencies, which will likely
be used instead once implemented.&lt;/p&gt;
&lt;h4 id=&quot;software-bill-of-materials-(sbom)&quot; tabindex=&quot;-1&quot;&gt;Software Bill of Materials (SBOM) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#software-bill-of-materials-(sbom)&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Sbom.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_sbom&lt;/code&gt;&lt;/a&gt;
job generates a
&lt;a href=&quot;https://github.com/CycloneDX/cyclonedx-gomod&quot;&gt;CycloneDX formatted SBOM&lt;/a&gt;, which
is an industry standard format for reporting dependencies used by an
application. Not that the SBOM generation is slightly different for the type:
&lt;code&gt;app&lt;/code&gt;, &lt;code&gt;mod&lt;/code&gt;, or &lt;code&gt;bin&lt;/code&gt;. See the documentation for complete details.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_sbom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!reference&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.go&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; image&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;SBOM_TYPE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;app&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;SBOM_ARGS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-main=./&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_mod_download
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/go.sum&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; go install github.com/CycloneDX/cyclonedx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gomod/cmd/cyclonedx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gomod@v1.4.1
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; cyclonedx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;gomod $SBOM_TYPE $SBOM_ARGS &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;json=true &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;licenses=true &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;output=sbom.json &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;output&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;version=1.4 $SBOM_PATH
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; sbom.json&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;deployment&quot; tabindex=&quot;-1&quot;&gt;Deployment &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#deployment&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/jobs/Go-Deploy.gitlab-ci.yml&quot;&gt;&lt;code&gt;go_deploy&lt;/code&gt;&lt;/a&gt;
job runs on tag pipelines if either &lt;code&gt;build&lt;/code&gt; job runs and publishes the binaries
to GitLab&#39;s
&lt;a href=&quot;https://docs.gitlab.com/ee/user/packages/generic_packages/&quot;&gt;generic package registry&lt;/a&gt;
under the git tag. An example showing the package registry for GitLab&#39;s
&lt;code&gt;release-cli&lt;/code&gt; tool can be seen
&lt;a href=&quot;https://gitlab.com/gitlab-org/release-cli/-/packages/18522079&quot;&gt;here&lt;/a&gt; (also the
inspiration for the job).&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;go_deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; .go
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# See https://docs.gitlab.com/ee/user/packages/generic_packages/#publish-a-package-file&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PACKAGE_REGISTRY_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/$CI_PROJECT_NAME&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_build
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; go_build_multi
      &lt;span class=&quot;token key atrule&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Deploy if tag pipeline and at least one build job was executed&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ($GO_BUILD_CURRENT &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; $GO_BUILD_MULTI) &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; $CI_COMMIT_TAG
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; apk add curl
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; cd $GO_BIN_DIR
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      for FILE in *; do
        echo &quot;Deploying: $FILE&quot;;
        curl --header &quot;JOB-TOKEN: ${CI_JOB_TOKEN}&quot; --upload-file ${FILE} ${PACKAGE_REGISTRY_URL}/${CI_COMMIT_TAG}/${FILE};
      done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the future this could be updated to publish to GitLab&#39;s
&lt;a href=&quot;https://docs.gitlab.com/ee/user/packages/go_proxy/&quot;&gt;Go proxy&lt;/a&gt;, but this is
currently experimental, likely to be for the foreseeable future, and is disabled
on gitlab.com.&lt;/p&gt;
&lt;h2 id=&quot;collection-of-ci-jobs&quot; tabindex=&quot;-1&quot;&gt;Collection of CI jobs &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#collection-of-ci-jobs&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To simplify use of the pipeline, a
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/blob/main/collections/Go-Build-Test-Deploy.gitlab-ci.yml&quot;&gt;&lt;code&gt;Go-Build-Test-Deploy&lt;/code&gt;&lt;/a&gt;
&lt;em&gt;collection&lt;/em&gt; has been created, including all of the Go jobs. As previously
discussed, each job contains &lt;code&gt;rules&lt;/code&gt; logic ensuring it only runs when
appropriate, so this allows an individual Go project&#39;s &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file to
be the following, and all appropriate jobs are run.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;gitlab-ci-utils/gitlab-ci-templates&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;ref&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;main&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/collections/Go-Build-Test-Deploy.gitlab-ci.yml&#39;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Optional variables to include the build and deploy jobs&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;GO_BUILD_CURRENT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;true&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;GO_BUILD_MULTI&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;true&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;Summary &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-go-projects/#summary&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post detailed a complete GitLab CI pipeline for Go projects, including
dependency management, code analysis, code quality, build, test, and deployment.
The pipeline is designed to be flexible, allowing jobs to be included or
excluded as desired, and optimized for parallel execution. It also includes
several reports to leverage GitLab capabilities for review of pipeline results.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>GitLab Semgrep SAST Analysis - But More</title>

    <link href="https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/" />
    <updated>2024-01-07T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/</id>
    <content type="html">&lt;p&gt;GitLab continues to migrate Static Application Security Testing (SAST) to
Semgrep, and makes this available to all GitLab tiers. This analysis only
includes the rules that GitLab manages, but there are many more available in the
&lt;a href=&quot;https://github.com/semgrep/semgrep-rules&quot;&gt;Semgrep Rules&lt;/a&gt; project. This post
details how to combine the two to get a more comprehensive analysis.&lt;/p&gt;
&lt;h2 id=&quot;gitlab-static-application-security-testing&quot; tabindex=&quot;-1&quot;&gt;GitLab Static Application Security Testing &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/#gitlab-static-application-security-testing&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For several years, since June 2020 with GitLab 13.3, GitLab has provided the
capability for Static Application Security Testing (SAST) to analyze code for
potential vulnerabilities in all GitLab tiers, including Free. There are some
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/sast/#summary-of-features-per-tier&quot;&gt;additional features&lt;/a&gt;
available in GitLab Ultimate, but the core analysis and customization of
scanners and settings is available to all.&lt;/p&gt;
&lt;p&gt;GitLab SAST supports over 20 languages and frameworks, and is
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/sast/#configure-sast-in-your-cicd-yaml&quot;&gt;easy to implement&lt;/a&gt;
by adding the following to your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Jobs/SAST.gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it. The SAST template has &lt;code&gt;rules&lt;/code&gt; configured to only run the appropriate
analyzers based on the languages used in the codebase (checked by file
extension). Each analyzer runs in a dedicated job to parallelize the work and
avoid the need for Docker-in-Docker.&lt;/p&gt;
&lt;p&gt;The most broadly used analyzer is &lt;a href=&quot;https://github.com/semgrep/semgrep&quot;&gt;Semgrep&lt;/a&gt;,
an open source engine for static code analysis, combined with a set of GitLab
developed and managed
&lt;a href=&quot;https://gitlab.com/gitlab-org/security-products/sast-rules&quot;&gt;SAST rules&lt;/a&gt;.
Semgrep currently covers about one third of the languages and frameworks
supported by GitLab&#39;s SAST analysis. GitLab also has a broader effort in work to
migrate all SAST analysis to Semgrep, as noted in
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/sast/analyzers.html#transition-to-semgrep-based-scanning&quot;&gt;the documentation&lt;/a&gt;
and tracked in &lt;a href=&quot;https://gitlab.com/groups/gitlab-org/-/epics/5245&quot;&gt;this epic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One challenge with GitLab only using their own managed rules is a limited number
of rules, which is more evident in some languages. Here are the current counts
of GitLab SAST rules for Semgrep by language.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Language&lt;/th&gt;
&lt;th style=&quot;text-align:center&quot;&gt;Rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;C&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;C#&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Go&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Java&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;JavaScript&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Python&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Scala&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;TypeScript&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Semgrep, however, has it&#39;s own set of rules, and many, many more rules in some
cases, as shown here for a subset of these languages.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Language&lt;/th&gt;
&lt;th style=&quot;text-align:center&quot;&gt;Rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Go&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;JavaScript&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;163&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;TypeScript&lt;/td&gt;
&lt;td style=&quot;text-align:center&quot;&gt;165&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This post details what is believed to be the best way to consolidate these rules
to have the most comprehensive SAST testing.&lt;/p&gt;
&lt;h2 id=&quot;use-semgrep-as-a-standalone-job&quot; tabindex=&quot;-1&quot;&gt;Use Semgrep as a standalone job? &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/#use-semgrep-as-a-standalone-job&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Semgrep documentation does have
&lt;a href=&quot;https://semgrep.dev/docs/semgrep-ci/sample-ci-configs/#gitlab-cicd&quot;&gt;instructions for running in GitLab CI&lt;/a&gt;,
so why not just use that? There are a few significant issues with that
implementation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There&#39;s a strong push to use
&lt;a href=&quot;https://semgrep.dev/docs/semgrep-ci/running-semgrep-ci-with-semgrep-cloud-platform/&quot;&gt;Semgrep Cloud Platform&lt;/a&gt;,
which provides a custom dashboard for managing rules, integration to provide
MR comments, additional features with their paid subscriptions, etc. The goal
for this use case is to augment what GitLab SAST already provides, so having
another tool to use to manage results is not desirable. And some of the
capabilities provided there, for example diff-aware scanning, links to code,
and MR comments are already better integrated through GitLab&#39;s existing merge
request capabilities, especially for GitLab Ultimate.&lt;/li&gt;
&lt;li&gt;The Semgrep rules are mix of several categories: security, best practices,
correctness, and maintainability. The goal of this job is SAST analysis, so
only the security rules are desired. For any of the languages of concern,
there are extremely capable language-specific tools for linting best
practices, correctness, and maintainability. Additionally, the GitLab SAST
results feed into a SAST report, managed through GitLab&#39;s
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/&quot;&gt;security capabilities&lt;/a&gt;,
and that workflow is not intended to manage other code quality resources (for
example, the option to for Security team approval for violations).&lt;/li&gt;
&lt;li&gt;In some cases, both GitLab and Semgrep have implemented the same rules. So,
using both would result in duplicate findings, and the overhead and
frustration that comes with that for SAST rules that are already notoriously
noisy.&lt;/li&gt;
&lt;li&gt;The Semgrep rules, either individually or as a collection, are not versioned.
The rules each have a SHA256 hash to verify integrity, and a last updated
timestamp, but nothing comparable to a &amp;quot;release&amp;quot; that indicates rule changes,
and the implications of those changes, to the user.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-combined-solution&quot; tabindex=&quot;-1&quot;&gt;The combined solution &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/#the-combined-solution&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So, a different solution was needed that addresses the concerns previously
identified. This solution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Builds on GitLab&#39;s &lt;code&gt;semgrep-sast&lt;/code&gt; job, which includes their Go-based analyzer
that wraps Semgrep, runs it with a specific set of rules, and provides results
in a GitLab SAST formatted report.&lt;/li&gt;
&lt;li&gt;Adds the applicable Semgrep security rules from the
&lt;a href=&quot;https://github.com/semgrep/semgrep-rules&quot;&gt;official repository&lt;/a&gt; (including any
associated example files). As noted previously, this repository has no tagged
versions, so the latest rules from the default branch are used.&lt;/li&gt;
&lt;li&gt;Identifies Semgrep rule changes to users, and is released with semantic
versioning.&lt;/li&gt;
&lt;li&gt;Eliminates rules duplicated between GitLab SAST and Semgrep. For simplicity,
and to make this project a pure extension of GitLab SAST, the GitLab rules are
used where there are duplicates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This led to the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus&quot;&gt;GitLab Semgrep Plus&lt;/a&gt;
project. The container image is derived from the GitLab Semgrep analyzer. The
&lt;code&gt;semgrep-rules/&lt;/code&gt; directory in the repository includes copies, unchanged, of all
security rules and examples from Semgrep rules for Go, JavaScript, and
TypeScript, which are included in the analysis. This makes changes to these
rules and code examples explicitly visible to users.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This could contain rules for the many other languages that Semgrep supports,
but these are the only languages currently in use in the
&lt;a href=&quot;https://gitlab.com/groups/gitlab-ci-utils&quot;&gt;GitLab CI Utils&lt;/a&gt; projects where
this was initiated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The rules, without examples, are included in the &lt;code&gt;/rules&lt;/code&gt; directory in the
container image. GitLab&#39;s Semgrep analyzer is designed to use all rules in that
directory, so no other changes are required to include them in the analysis. A
shell script with all of the applicable logic is used to update the rules so
that the changes are made consistently with an automated process.&lt;/p&gt;
&lt;h3 id=&quot;managing-ongoing-semgrep-rule-changes&quot; tabindex=&quot;-1&quot;&gt;Managing ongoing Semgrep rule changes &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/#managing-ongoing-semgrep-rule-changes&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The project is setup to automatically detect changes to the Semgrep Rules
project and incorporate any changes to security rules for the specified
languages.&lt;/p&gt;
&lt;p&gt;Since Semgrep Rules does not identify versions,
&lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt; is used to track the latest git ref of
the development branch, and it initiates a merge request with any ref changes.
The CI pipeline is setup with a trigger job to check for rule updates (if that
check has not been done). This job is triggered in another project to isolate
the credentials that can push commits back to this project.&lt;/p&gt;
&lt;p&gt;The triggered pipeline clones this project, clones the Semgrep Rules project,
re-runs the shell script to update rules, and pushes any updates. The commit
uses &lt;code&gt;--amend&lt;/code&gt; to leave only one commit with an updated message, and is made
with Renovate bot account, so Renovate continues to propagate changes to Semgrep
Rules if the merge request is not immediately merged. The commit message is also
updated to indicate rule changes were incorporated. Since the updated Semgrep
rules are pushed, a new pipeline is triggered, and the updated commit message is
used in the job &lt;code&gt;rules&lt;/code&gt; to stop an infinite loop of triggered rule update
pipelines.&lt;/p&gt;
&lt;h2 id=&quot;usage-in-gitlab-ci&quot; tabindex=&quot;-1&quot;&gt;Usage in GitLab CI &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-semgrep-sast-analysis-but-more/#usage-in-gitlab-ci&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The GitLab SAST template is already setup to specify the container image for all
SAST jobs via &lt;code&gt;variables&lt;/code&gt;, so the implementation in GitLab CI is simply:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Jobs/SAST.gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci.yml

&lt;span class=&quot;token key atrule&quot;&gt;semgrep-sast&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;SAST_ANALYZER_IMAGE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;semgrep&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;plus&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that the Semgrep rules are included in the analysis, and the corresponding
report.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>The Year in Review 2023</title>

    <link href="https://aarongoldenthal.com/posts/the-year-in-review-2023/" />
    <updated>2023-12-29T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/the-year-in-review-2023/</id>
    <content type="html">&lt;p&gt;A lot has happened in the last year - the resurgence of this blog, I created six
new software projects, and I shipped almost 200 software releases. In addition,
I learned Go, started publishing NPM packages with provenance, and generative AI
and large language models took over the world. This post reviews it all.&lt;/p&gt;
&lt;h2 id=&quot;blog-posts&quot; tabindex=&quot;-1&quot;&gt;Blog posts &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2023/#blog-posts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The year brought with it the return of this blog. I had a goal of averaging two
blog posts per month. That certainly seemed achievable, but the result didn&#39;t
quite measure up - a grand total of 10 posts (including this, and the kickoff -
not sure those really count), averaging less than one per month. Even though
that didn&#39;t meet my original goal, I feel good about the results. The posts were
either detailing significant new software releases or solutions to significant
GitLab, CI/CD, or DevOps problems. And a lot of research was done on a few more
big ones coming in 2024, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An enhanced GitLab Semgrep SAST (static application security testing) job with
several hundred additional rules.&lt;/li&gt;
&lt;li&gt;A comprehensive GitLab CI pipeline for Go projects.&lt;/li&gt;
&lt;li&gt;An assessment of issues with GitLab&#39;s Code Quality template and the
alternative I&#39;ve been using for the last 6 months (it&#39;s getting long, it will
probably be a 2 or 3 part series).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;software-projects&quot; tabindex=&quot;-1&quot;&gt;Software projects &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2023/#software-projects&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most of this year was busy with open source software projects (probably why I
didn&#39;t meet my original blog post goal). I started the following six new
projects this year:&lt;/p&gt;
&lt;table class=&quot;first-column-250&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd&quot;&gt;GitLab PMD CPD&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A container image to run PMD&#39;s Copy/Paste Detector (CPD) and convert the results to GitLab Code Quality report format. This provides an alternative to GitLab&#39;s built-in Code Quality template for identifying duplicate code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus&quot;&gt;GitLab Semgrep Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;GitLab&#39;s &lt;code&gt;semgrep&lt;/code&gt; container image augmented with hundreds of additional Node.js/JavaScript/Typescript and Go rules from &lt;a href=&quot;https://github.com/semgrep/semgrep-rules&quot;&gt;Semgrep&#39;s rule repository&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ui-plus&quot;&gt;GitLab UI Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A Chrome browser extension to augment the GitLab UI for improved UX (very much a work in progress).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test&quot;&gt;Go Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A container image with OS packages and Go tools necessary for performing Go testing and producing testing reports for GitLab.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse&quot;&gt;Lighthouse&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;A container image to run &lt;a href=&quot;https://www.npmjs.com/package/lighthouse&quot;&gt;Lighthouse&lt;/a&gt; and &lt;a href=&quot;https://www.npmjs.com/package/@lhci/cli&quot;&gt;Lighthouse CI&lt;/a&gt; with the latest Chromium release.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft&quot;&gt;Syft&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;An Alpine Linux based container image to run &lt;a href=&quot;https://github.com/anchore/syft&quot;&gt;Syft&lt;/a&gt; to allow Software Bill of Materials (SBOMs) generation in GitLab CI where a shell is required.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Between those projects and my other existing projects I shipped &lt;strong&gt;199
releases&lt;/strong&gt;.&lt;/p&gt;
&lt;table class=&quot;first-column-250&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Project&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Releases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester&quot;&gt;Bin Tester&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/4.0.1&quot;&gt;4.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger&quot;&gt;CI Logger&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/ci-logger/-/releases/5.1.1&quot;&gt;5.1.1&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files&quot;&gt;Config Files&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;8&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/9.2.0&quot;&gt;9.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/9.1.0&quot;&gt;9.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/9.0.1&quot;&gt;9.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/9.0.0&quot;&gt;9.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/7.2.0&quot;&gt;7.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/7.1.0&quot;&gt;7.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/config-files/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check&quot;&gt;Docker Dependency Check&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;19&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.4&quot;&gt;2.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.3&quot;&gt;2.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.2&quot;&gt;2.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.12&quot;&gt;1.16.12&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.11&quot;&gt;1.16.11&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.10&quot;&gt;1.16.10&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.9&quot;&gt;1.16.9&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.8&quot;&gt;1.16.8&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.7&quot;&gt;1.16.7&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.6&quot;&gt;1.16.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.5&quot;&gt;1.16.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.4&quot;&gt;1.16.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.3&quot;&gt;1.16.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.2&quot;&gt;1.16.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.1&quot;&gt;1.16.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.16.0&quot;&gt;1.16.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-dependency-check/-/releases/1.15.9&quot;&gt;1.15.9&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-hugo&quot;&gt;Docker Hugo&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-hugo/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts&quot;&gt;Docker PowerShell Scripts&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/4.5.0&quot;&gt;4.5.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/4.4.0&quot;&gt;4.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/4.3.0&quot;&gt;4.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/4.2.0&quot;&gt;4.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-powershell-scripts/-/releases/4.1.0&quot;&gt;4.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer&quot;&gt;Docker Puppeteer&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;7&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/11.0.0&quot;&gt;11.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/10.2.0&quot;&gt;10.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/10.1.0&quot;&gt;10.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/10.0.0&quot;&gt;10.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/9.3.0&quot;&gt;9.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/9.2.0&quot;&gt;9.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-puppeteer/-/releases/9.1.0&quot;&gt;9.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates&quot;&gt;Docker Sokrates&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/2.1.1&quot;&gt;2.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/docker-sokrates/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq&quot;&gt;Docker curl jq&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/curl-jq/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard&quot;&gt;ESLint Config Standard&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;12&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/25.0.0&quot;&gt;25.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/24.0.1&quot;&gt;24.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/24.0.0&quot;&gt;24.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/23.1.1&quot;&gt;23.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/23.1.0&quot;&gt;23.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/23.0.0&quot;&gt;23.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/22.1.0&quot;&gt;22.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/22.0.0&quot;&gt;22.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/21.0.0&quot;&gt;21.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/20.1.0&quot;&gt;20.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/20.0.0&quot;&gt;20.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/eslint-config-standard/-/releases/19.0.2&quot;&gt;19.0.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env&quot;&gt;GitLab CI Env&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/9.1.0&quot;&gt;9.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/9.0.0&quot;&gt;9.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/8.1.0&quot;&gt;8.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/8.0.0&quot;&gt;8.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-env/-/releases/7.1.0&quot;&gt;7.1.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates&quot;&gt;GitLab CI Templates&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;40&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/26.0.0&quot;&gt;26.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/25.0.0&quot;&gt;25.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/24.2.0&quot;&gt;24.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/24.1.0&quot;&gt;24.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/24.0.1&quot;&gt;24.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/24.0.0&quot;&gt;24.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/23.1.0&quot;&gt;23.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/23.0.0&quot;&gt;23.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/22.2.0&quot;&gt;22.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/22.1.0&quot;&gt;22.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/22.0.0&quot;&gt;22.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/21.0.2&quot;&gt;21.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/21.0.1&quot;&gt;21.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/21.0.0&quot;&gt;21.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/20.2.1&quot;&gt;20.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/20.2.0&quot;&gt;20.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/20.1.0&quot;&gt;20.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/20.0.0&quot;&gt;20.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/19.1.1&quot;&gt;19.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/19.1.0&quot;&gt;19.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/19.0.0&quot;&gt;19.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/18.1.0&quot;&gt;18.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/18.0.1&quot;&gt;18.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/18.0.0&quot;&gt;18.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/17.0.1&quot;&gt;17.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/17.0.0&quot;&gt;17.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.3.0&quot;&gt;16.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.2.0&quot;&gt;16.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.1.0&quot;&gt;16.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.0.3&quot;&gt;16.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.0.2&quot;&gt;16.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.0.1&quot;&gt;16.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/16.0.0&quot;&gt;16.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.3.1&quot;&gt;15.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.3.0&quot;&gt;15.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.2.2&quot;&gt;15.2.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.2.1&quot;&gt;15.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.2.0&quot;&gt;15.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.1.0&quot;&gt;15.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-ci-templates/-/releases/15.0.0&quot;&gt;15.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd&quot;&gt;GitLab PMD CPD&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/0.6.0&quot;&gt;0.6.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/0.5.0&quot;&gt;0.5.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci&quot;&gt;GitLab Pa11y CI&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;7&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.4.0&quot;&gt;7.4.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.3.1&quot;&gt;7.3.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.3.0&quot;&gt;7.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.2.0&quot;&gt;7.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.1.1&quot;&gt;7.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.1.0&quot;&gt;7.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pa11y-ci/-/releases/7.0.3&quot;&gt;7.0.3&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline&quot;&gt;GitLab Pipeline Timeline&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline/-/releases/0.2.1&quot;&gt;0.2.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pipeline-timeline/-/releases/0.2.0&quot;&gt;0.2.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser&quot;&gt;GitLab Releaser&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/7.0.1&quot;&gt;7.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/7.0.0&quot;&gt;7.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/5.0.1&quot;&gt;5.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/4.0.4&quot;&gt;4.0.4&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus&quot;&gt;GitLab Semgrep Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;11&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/3.3.0&quot;&gt;3.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/3.2.0&quot;&gt;3.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/3.1.0&quot;&gt;3.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/1.0.3&quot;&gt;1.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-semgrep-plus/-/releases/0.5.0&quot;&gt;0.5.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test&quot;&gt;Go Test&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/1.0.2&quot;&gt;1.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/1.0.1&quot;&gt;1.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/go-test/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse&quot;&gt;Lighthouse&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;23&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.3.0&quot;&gt;4.3.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.2.0&quot;&gt;4.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.1.1&quot;&gt;4.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.1.0&quot;&gt;4.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/3.2.0&quot;&gt;3.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/3.1.0&quot;&gt;3.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/3.0.2&quot;&gt;3.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.1.1&quot;&gt;2.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.1.0&quot;&gt;2.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.6&quot;&gt;2.0.6&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.5&quot;&gt;2.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.4&quot;&gt;2.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.3&quot;&gt;2.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.2&quot;&gt;2.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/1.2.0&quot;&gt;1.2.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary&quot;&gt;Pa11y CI CLI Summary Reporter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;1&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-cli-summary/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html&quot;&gt;Pa11y CI HTML Reporter&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;7&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/6.0.2&quot;&gt;6.0.2&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/6.0.1&quot;&gt;6.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/6.0.0&quot;&gt;6.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/5.1.1&quot;&gt;5.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/5.1.0&quot;&gt;5.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/5.0.4&quot;&gt;5.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-html/-/releases/5.0.3&quot;&gt;5.0.3&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner&quot;&gt;Pa11y CI Reporter Runner&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/3.0.1&quot;&gt;3.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/3.0.0&quot;&gt;3.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/-/releases/2.0.5&quot;&gt;2.0.5&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus&quot;&gt;Pa11y HTML Reporter Plus&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;6&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/2.0.1&quot;&gt;2.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/2.0.0&quot;&gt;2.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/1.1.1&quot;&gt;1.1.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/1.0.5&quot;&gt;1.0.5&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pa11y-reporter-html-plus/-/releases/1.0.4&quot;&gt;1.0.4&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean&quot;&gt;Pagean&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;4&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/9.0.0&quot;&gt;9.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/8.0.4&quot;&gt;8.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/8.0.3&quot;&gt;8.0.3&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/pagean/-/releases/8.0.2&quot;&gt;8.0.2&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog&quot;&gt;Releaselog&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/5.0.0&quot;&gt;5.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/4.0.1&quot;&gt;4.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/4.0.0&quot;&gt;4.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/3.0.4&quot;&gt;3.0.4&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/releaselog/-/releases/3.0.3&quot;&gt;3.0.3&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard&quot;&gt;StyleLint Config Standard&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;5&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/16.0.0&quot;&gt;16.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/15.0.0&quot;&gt;15.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/14.0.0&quot;&gt;14.0.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/13.0.1&quot;&gt;13.0.1&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/node/stylelint-config-standard/-/releases/13.0.0&quot;&gt;13.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft&quot;&gt;Syft&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;2&lt;/strong&gt; (&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.1.0&quot;&gt;1.1.0&lt;/a&gt;, &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/-/releases/1.0.0&quot;&gt;1.0.0&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;…and that&#39;s not even counting the simple app I put together to pull all of
this new project and software releases data from the GitLab API and format it
😉.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A noteworthy aspect is that seven of my NPM packages are now
&lt;a href=&quot;https://github.blog/2023-04-19-introducing-npm-package-provenance/&quot;&gt;published with package provenance&lt;/a&gt;,
with the others setup and waiting on their next release. It&#39;s super easy to
setup (for example see
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/signing_examples.html#use-sigstore-and-npm-to-generate-keyless-provenance&quot;&gt;the GitLab instructions&lt;/a&gt;),
and has significant potential for improving the security of the NPM ecosystem.
So, if you&#39;re not doing it, why not?&lt;/p&gt;
&lt;p&gt;This was also the year of generative AI and large language models. Do I think
they&#39;ll replace developers and we should all be afraid for our jobs? &lt;strong&gt;No&lt;/strong&gt;. Do
I think I was more productive with at least moderate use of GitHub Copilot and
ChatGPT (my two tools of choice)? &lt;strong&gt;Yes&lt;/strong&gt; - it&#39;s like having an intern to help,
with all the good and bad that comes with that.&lt;/p&gt;
&lt;h2 id=&quot;i&#39;m-a-gopher&quot; tabindex=&quot;-1&quot;&gt;I&#39;m a Gopher &lt;img src=&quot;https://aarongoldenthal.com/img/gopher.webp&quot; width=&quot;28&quot; height=&quot;28&quot; alt=&quot;the Go gopher icon&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;border-width:0;display:inline;vertical-align:baseline;&quot;&gt; &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2023/#i&#39;m-a-gopher&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Last, but certainly not least, on my list of accomplishments this year was
learning Go, putting together a viable CI/CD pipeline for Go in GitLab, and
releasing a Go application
(&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd/-/releases/2.0.0&quot;&gt;GitLab PMD CPD v2.0.0&lt;/a&gt;).
I&#39;m sure I won&#39;t abandon Node any time soon, but I do like many things about Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go doesn&#39;t try to be everything for everyone. It is what it is, it&#39;s
opinionated, and that&#39;s okay.&lt;/li&gt;
&lt;li&gt;Go has simple syntax, with fewer than 30 keywords.&lt;/li&gt;
&lt;li&gt;Every language should have &lt;code&gt;defer&lt;/code&gt; and the simplicity of concurrency that
comes with a &lt;code&gt;goroutine&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Go standard library has extensive capabilities, limiting the need for
including other dependencies. Coming from working on mostly Node projects for
a while, and the mess that is NPM dependencies, being able to write a
functional Go application with no dependencies was a little shocking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are also a lot of aspects that are… … … I&#39;ll go with &amp;quot;quirky.&amp;quot; I&#39;m sure
I&#39;ll write more about that at some point.&lt;/p&gt;
&lt;h2 id=&quot;looking-forward-to-2024&quot; tabindex=&quot;-1&quot;&gt;Looking forward to 2024 &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-year-in-review-2023/#looking-forward-to-2024&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I think the only thing certain at this point is uncertainty, but here are a few
things that I am looking forward to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The release of &lt;a href=&quot;https://github.com/pa11y/pa11y&quot;&gt;&lt;code&gt;pa11y@7.0.0&lt;/code&gt;&lt;/a&gt; and
&lt;a href=&quot;https://github.com/pa11y/pa11y-ci&quot;&gt;&lt;code&gt;pa11y-ci@4.0.0&lt;/code&gt;&lt;/a&gt; should be in early 2024,
bringing both tools back to what I&#39;d call a sustained state. I&#39;ve made
numerous contributions to both projects, and have several of my own that are
plugins to &lt;code&gt;pa11y&lt;/code&gt;/&lt;code&gt;pa11y-ci&lt;/code&gt;, so I&#39;m excited to see this milestone.&lt;/li&gt;
&lt;li&gt;I&#39;m not convinced that the Node ecosystem will ever fully make the move from
CommonJS to ESM, as much as some people want that. But, in the interest of
moving forward I do plan to migrate all of my CLI-based Node projects to ESM
(not libraries). That may also include the move to TypeScript, we&#39;ll see…&lt;/li&gt;
&lt;li&gt;I mentioned it in preceding sections, but 2023 brought a lot of research and
some new projects to optimize GitLab CI pipelines, so I am looking forward to
finishing those posts and sharing the results.&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
    
    <entry>
    <title>Managing GitLab License Policies With No Dependencies</title>

    <link href="https://aarongoldenthal.com/posts/managing-gitlab-license-policies-with-no-dependencies/" />
    <updated>2023-11-17T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/managing-gitlab-license-policies-with-no-dependencies/</id>
    <content type="html">&lt;p&gt;GitLab&#39;s
&lt;a href=&quot;https://docs.gitlab.com/ee/user/compliance/license_approval_policies.html&quot;&gt;license approval policies&lt;/a&gt;
provide a powerful and flexible means of managing dependency license approvals.
Using these policies requires performing GitLab&#39;s Dependency Scanning to
generate and report a Software Bill of Materials (SBOM). For the cases where
there are no dependencies, Dependency Scanning is not run, no SBOM is created,
and the license policies are considered failed and require approval. For
projects without dependencies this is an added hassle, and this post proposes a
solution.&lt;/p&gt;
&lt;h2 id=&quot;the-solution-for-no-dependencies&quot; tabindex=&quot;-1&quot;&gt;The solution for no dependencies &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-gitlab-license-policies-with-no-dependencies/#the-solution-for-no-dependencies&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In GitLab 16.4, license approval policies were added as an enhancement to the
previous License Scanning template.
&lt;a href=&quot;https://docs.gitlab.com/ee/user/compliance/license_scanning_of_cyclonedx_files/&quot;&gt;The documentation&lt;/a&gt;
detailing the implementation indicates that Dependency Scanning should be
enabled, then add the appropriate license approval policies, and the policies
are enforced. Unfortunately, the Dependency Scanning job &lt;code&gt;rules&lt;/code&gt; are optimized
to only run when there are dependency lockfiles present, so when they are not it
does not run, no SBOM is created, and the license approval policies fail and
require approval even though there are no dependencies. Not the end of the
world, but a hassle.&lt;/p&gt;
&lt;p&gt;The solution for these cases is simple, and is actually captured in the GitLab
documentation - ensure that a CycloneDX SBOM is created in some job and provided
to GitLab as &lt;code&gt;reports:cyclonedx&lt;/code&gt;. This then triggers license scanning using the
contents of the SBOM.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This post covers non container-based projects, otherwise Container Scanning
should be run to provide an SBOM.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;always-run-dependency-scanning&quot; tabindex=&quot;-1&quot;&gt;Always run dependency scanning? &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-gitlab-license-policies-with-no-dependencies/#always-run-dependency-scanning&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;One option investigated is simply overriding the rules for the Dependency
Scanning template to run on all pipelines. Theoretically this should produce an
SBOM with no dependencies, which is valid. Unfortunately, attempts with multiple
projects showed inconsistent results. In some cases the SBOM was created with no
dependencies, as expected, but in others no SBOM was created and no errors were
identified by Gemnasium (GitLab&#39;s Dependency Scanning tool). There was no
obvious commonality for these two cases.&lt;/p&gt;
&lt;h3 id=&quot;syft-to-the-rescue&quot; tabindex=&quot;-1&quot;&gt;Syft to the rescue &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-gitlab-license-policies-with-no-dependencies/#syft-to-the-rescue&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;So, another tool must be used to generate the SBOM. GitLab simply expects a
CycloneDX formatted SBOM, a well established standard. The chosen solution here
is the &lt;a href=&quot;https://github.com/anchore/syft&quot;&gt;Syft&lt;/a&gt; application from
&lt;a href=&quot;https://anchore.com/&quot;&gt;Anchore&lt;/a&gt;. Syft is designed to create SBOMs from a host of
different ecosystems, scanning either container images or file systems. It
includes support for C/C++, Go, Java, JavaScript, Python, Ruby, Rust, Swift, and
others. See the documentation on
&lt;a href=&quot;https://github.com/anchore/syft#supported-ecosystems&quot;&gt;supported ecosystems&lt;/a&gt; for
complete details. There are many other tools available, but Syft seemed to
provide the most comprehensive single solution, and in tests consistently
produced a valid CycloneDX SBOM with or without dependencies.&lt;/p&gt;
&lt;p&gt;There is a
&lt;a href=&quot;https://hub.docker.com/r/anchore/syft&quot;&gt;Syft container images on Docker Hub&lt;/a&gt;,
but this image is built &lt;code&gt;from scratch&lt;/code&gt; and has no shell available (see the
&lt;a href=&quot;https://github.com/anchore/syft/blob/main/Dockerfile&quot;&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt;). Using an
image with no shell in GitLab CI results in a log like the following because
GitLab requires a shell to execute the &lt;code&gt;script&lt;/code&gt; (see
&lt;a href=&quot;https://docs.gitlab.com/runner/shells/&quot;&gt;supported shells&lt;/a&gt;):&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;Executing &lt;span class=&quot;token string&quot;&gt;&quot;step_script&quot;&lt;/span&gt; stage of the job script
Using &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; image sha256:c9b97dbf39ec0138c627c8bd3b56e5d879bc0b8304f05db9dde6db9daff6d129
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; anchore/syft:v0.97.1 with digest
  anchore/syft@sha256:abc8d4310c54b56dd1e789d5f60b8ebc43f472652b34971d4b0d0dbed7f4ebda
Cleaning up project directory and &lt;span class=&quot;token function&quot;&gt;file&lt;/span&gt; based variables
ERROR: Job failed &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;system failure&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;: Error response from daemon: OCI runtime create failed:
  container_linux.go:380: starting container process caused: exec: &lt;span class=&quot;token string&quot;&gt;&quot;sh&quot;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt;
  executable &lt;span class=&quot;token function&quot;&gt;file&lt;/span&gt; not found &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token environment constant&quot;&gt;$PATH&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; unknown &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exec.go:78:0s&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, a
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/container-images/syft/&quot;&gt;new Syft image&lt;/a&gt; was
created that includes Syft in an Alpine Linux image so a shell is available. The
details of this image are not addressed here, but feel free to post questions in
the comments (or the project issues).&lt;/p&gt;
&lt;h2 id=&quot;the-gitlab-ci-job&quot; tabindex=&quot;-1&quot;&gt;The GitLab CI job &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-gitlab-license-policies-with-no-dependencies/#the-gitlab-ci-job&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First, setup a job to simply run Syft and provide an SBOM:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;syft_sbom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/container&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;images/syft&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /syft/syft $CI_PROJECT_DIR &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;o cyclonedx&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;json=syft.cdx.json
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; syft.cdx.json
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;cyclonedx&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; syft.cdx.json&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uses the new Syft image noted previously, with the &lt;code&gt;entrypoint&lt;/code&gt; overridden
to nothing. Syft is executed in the &lt;code&gt;script&lt;/code&gt; with options to scan the current
directory and produce a CycloneDX formatted file. This is saved as an artifact
and uploaded as &lt;code&gt;reports:cyclonedx&lt;/code&gt; to satisfy the license approval policy
needs. This job could be used to always generate an SBOM, but in this example
Dependency Scanning is be allowed to do its job where it can, and just use Syft
to fill in the cases with no dependencies.&lt;/p&gt;
&lt;p&gt;To finalize the &lt;code&gt;rules&lt;/code&gt; for when Syft should be run, first examine the
&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/blob/09c120d36717697cb2993ef590d647cd6ce0b2c4/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml&quot;&gt;Dependency Scanning template rules&lt;/a&gt;.
A subset is included below for the primary Gemnasium job.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;.gemnasium-shared-rule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/Gemfile.lock&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/composer.lock&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/gems.locked&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/go.sum&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/npm-shrinkwrap.json&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/package-lock.json&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/yarn.lock&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/pnpm-lock.yaml&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/packages.lock.json&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;**/conan.lock&#39;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;gemnasium-dependency_scanning&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $DEPENDENCY_SCANNING_DISABLED == &#39;true&#39; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; $DEPENDENCY_SCANNING_DISABLED == &#39;1&#39;
      &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $DS_EXCLUDED_ANALYZERS =~ /gemnasium(&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;^&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;$)/
      &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          $GITLAB_FEATURES =~ /&#92;bdependency_scanning&#92;b/ &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          $CI_GITLAB_FIPS_MODE == &#39;true&#39;
      &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!reference&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.gemnasium&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;rule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exists&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;DS_IMAGE_SUFFIX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-fips&#39;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;DS_REMEDIATE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;false&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          $GITLAB_FEATURES =~ /&#92;bdependency_scanning&#92;b/
      &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!reference&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.gemnasium&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;rule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exists&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&#39;s a rule template identifying the dependency lockfiles that triggers the
job (this is important later), then a standard set of rules used in GitLab
security scans:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An option for a variable &lt;code&gt;DEPENDENCY_SCANNING_DISABLED&lt;/code&gt; to disable the
Dependency Scanning job&lt;/li&gt;
&lt;li&gt;An option to disable Gemnasium via the &lt;code&gt;DS_EXCLUDED_ANALYZERS&lt;/code&gt; variable&lt;/li&gt;
&lt;li&gt;Otherwise, if &lt;code&gt;dependency_scanning&lt;/code&gt; is an available feature (the
&lt;code&gt;GITLAB_FEATURES&lt;/code&gt; variable is set by GitLab based on the license), execute the
job if one of the available lockfiles exists (with both FIPS and non-FIPS
variants)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are similar jobs for the Maven and Python Gemnasium scanners that follow
the same pattern.&lt;/p&gt;
&lt;p&gt;Pulling this all together is a CI file that includes both the Dependency
Scanning template and the Syft job defined previously, with overridden &lt;code&gt;rules&lt;/code&gt;
to only run in the appropriate cases.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Dependency&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Scanning.gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci.yml
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /jobs/Syft.gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci.yml

&lt;span class=&quot;token comment&quot;&gt;# Included from Syft.gitlab-ci.yml - the job defined above&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;syft_sbom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# If any of of these files exist, Gemnasium will be run and create an SBOM&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!reference&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.gemnasium&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;rule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exists&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never
    &lt;span class=&quot;token comment&quot;&gt;# If any of of these files exist, Gemnasium-maven will be run and create an SBOM&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!reference&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.gemnasium&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;maven&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;rule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exists&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never
    &lt;span class=&quot;token comment&quot;&gt;# If any of of these files exist, Gemnasium-python will be run and create an SBOM&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!reference&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.gemnasium&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;python&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shared&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;rule&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exists&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; never
    &lt;span class=&quot;token comment&quot;&gt;# Otherwise, run Syft to create an SBOM, even if empty&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case use the &lt;code&gt;.gemnasium-shared-rule*&lt;/code&gt; templates defined in the
Dependency Scanning template and setup &lt;code&gt;rules&lt;/code&gt; to never run the Syft job if any
of them are present, then finally a rule to run on any other branch pipelines
(to match the GitLab template, although this could include tags, MRs, etc).&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Managing Alpine Linux Based Container Images With Renovate</title>

    <link href="https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/" />
    <updated>2023-06-30T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/</id>
    <content type="html">&lt;p&gt;With it&#39;s default configuration, Renovate does a great job of managing container
image tag updates. One limitation is that for updates that include an OS in the
tag, for example the Alpine Linux version in &lt;code&gt;python:3.11.3-alpine3.17&lt;/code&gt;,
Renovate only updates Python image tags matching that version, which may limit
updates, and doesn&#39;t identify any OS updates. This post details how to configure
Renovate to work around that limitation for Alpine images.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This post assumes you have a working knowledge of Renovate and covers some
more advanced topics. You can find more introductory information in the
&lt;a href=&quot;https://docs.renovatebot.com/reading-list/&quot;&gt;Renovate docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;docker-versioning-overview&quot; tabindex=&quot;-1&quot;&gt;Docker versioning overview &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#docker-versioning-overview&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Container image tag versions can be wildly different, sometimes chaotic, but
there are some general conventions. Renovate tries its best to leverage them
with &lt;code&gt;docker&lt;/code&gt; versioning, which is the default for all container image tags.&lt;/p&gt;
&lt;p&gt;Renovate treats any SemVer-like initial number as the &amp;quot;version,&amp;quot; and it
maintains the &amp;quot;version&amp;quot; with the given specificity, so:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node:18&lt;/code&gt; is updated to &lt;code&gt;node:19&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node:18.16&lt;/code&gt; is updated to &lt;code&gt;node:18.17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node:18.16.0&lt;/code&gt; is updated to &lt;code&gt;node:18.16.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anything after the first hyphen in an image tag is treated as the
&amp;quot;compatibility&amp;quot; indicator. Renovate only identifies updates that maintain this
&amp;quot;compatibility.&amp;quot; So &lt;code&gt;node:18.16.0-alpine&lt;/code&gt; is updated to another &lt;code&gt;-alpine&lt;/code&gt; image,
for example &lt;code&gt;node:18.16.1-alpine&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Complete details are available in the
&lt;a href=&quot;https://docs.renovatebot.com/modules/versioning/#docker-versioning&quot;&gt;Renovate docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;docker-versioning-and-os-versions&quot; tabindex=&quot;-1&quot;&gt;Docker versioning and OS versions &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#docker-versioning-and-os-versions&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In most cases maintaining image &amp;quot;compatibility&amp;quot; is desired, as you wouldn&#39;t want
&lt;code&gt;python:3.11.3-slim-bullseye&lt;/code&gt; updated to &lt;code&gt;python:3.11.4-bullseye&lt;/code&gt; or
&lt;code&gt;python:3.11.4-alpine&lt;/code&gt; since they&#39;re completely different base images.&lt;/p&gt;
&lt;p&gt;This can also be a challenge, though, in cases where the OS version changes. For
example &lt;code&gt;python:3.11.3-alpine&lt;/code&gt; is updated to &lt;code&gt;python:3.11.4-alpine&lt;/code&gt;, but what if
you&#39;re using a specific OS version, like &lt;code&gt;python:3.11.3-alpine3.16&lt;/code&gt;? You might
notice that &lt;code&gt;python&lt;/code&gt; updates stop appearing even though new &lt;code&gt;python&lt;/code&gt; images are
released. In this case the Python v3.11.3 image was the last version released on
Alpine 3.16, so Renovate doesn&#39;t recognize any newer image tags that maintain
&amp;quot;compatibility,&amp;quot; and there are no further updates.&lt;/p&gt;
&lt;p&gt;The other challenge is that images with updated OS versions are not identified
as updates, so you might not know that Alpine 3.17 images were available
starting with Python 3.7.15, or Alpine 3.18 images were available starting with
Python 3.7.16. The same issue exists with other OSs, including Debian and
Ubuntu. If you&#39;re using &lt;code&gt;python:3.11.4-bullseye&lt;/code&gt;, Renovate does not identify an
update to &lt;code&gt;python:3.11.4-bookworm&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-track-os-versions&quot; tabindex=&quot;-1&quot;&gt;Why track OS versions &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#why-track-os-versions&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Given the previously identified issues, why would someone want to track specific
OS versions if it can prevent future updates? As with almost everything in
software, different use cases drive different solutions.&lt;/p&gt;
&lt;p&gt;As an example from Docker Hub, the following tags all point to the latest
&lt;code&gt;python&lt;/code&gt; image: &lt;code&gt;alpine&lt;/code&gt;, &lt;code&gt;3-alpine&lt;/code&gt;, &lt;code&gt;3.11-alpine&lt;/code&gt;, &lt;code&gt;3.11.4-alpine&lt;/code&gt;,
&lt;code&gt;3.11.4-alpine3.18&lt;/code&gt;. If the uses case is fairly generic, for example a
Python-based linting tool, it may not be overly sensitive to Python or Alpine
changes, so using &lt;code&gt;python:alpine&lt;/code&gt; may be appropriate. For something like the
base image for a containerized application, it&#39;s best to be as specific as
possible. This causes Renovate to identify any changes - application or OS - and
make the appropriate update, initiating a CI pipeline. This ensures any change
is tested before being merged into the codebase.&lt;/p&gt;
&lt;h2 id=&quot;the-workaround&quot; tabindex=&quot;-1&quot;&gt;The workaround &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#the-workaround&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The way to work around the Renovate constraints is to keep the &lt;code&gt;docker&lt;/code&gt; manager
for the &lt;code&gt;docker&lt;/code&gt; versioning, and add a
&lt;a href=&quot;https://docs.renovatebot.com/modules/manager/regex/&quot;&gt;&lt;code&gt;regex&lt;/code&gt; manager&lt;/a&gt; to setup
a separate versioning mechanism for the Alpine changes. The &lt;code&gt;regex&lt;/code&gt; manager
allows a fully customized Renovate configuration to manage dependencies that the
other managers do not cover, and is the only way to have Renovate version the
same dependency multiple ways. It also requires more complex configuration,
which is detailed below.&lt;/p&gt;
&lt;p&gt;Thanks to the Renovate team for
&lt;a href=&quot;https://github.com/renovatebot/renovate/discussions/22836&quot;&gt;their help&lt;/a&gt; sorting
out this solution.&lt;/p&gt;
&lt;h3 id=&quot;image-tag-format&quot; tabindex=&quot;-1&quot;&gt;Image tag format &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#image-tag-format&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As noted in the preceding example, there is not strict versioning in container
image tags, so investigating tags of interest in tracking revealed the following
list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker:23.0.6-alpine3.17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker:23.0.6-dind-alpine3.17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node:18.16.0-alpine3.17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python:3.11.4-alpine3.17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mcr.microsoft.com/powershell:7.3-alpine-3.17&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The example below covers these cases, but other image tags may drive Renovate
configuration tweaks.&lt;/p&gt;
&lt;h3 id=&quot;renovate-configuration&quot; tabindex=&quot;-1&quot;&gt;Renovate configuration &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#renovate-configuration&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The following section details the configuration of the &lt;code&gt;regex&lt;/code&gt; manager to
version the Alpine OS. For any values that are regular expressions, they&#39;re
specified as the JSON string representation of the regular expression, so any
&lt;code&gt;&#92;&lt;/code&gt; must be escaped as &lt;code&gt;&#92;&#92;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.renovatebot.com/configuration-options/#filematch&quot;&gt;&lt;code&gt;fileMatch&lt;/code&gt;&lt;/a&gt;
property is required and identifies regular expressions to match files to check.
In this case set it to &lt;code&gt;[&amp;quot;&#92;&#92;.gitlab-ci&#92;&#92;.yml$&amp;quot;, &amp;quot;Dockerfile&amp;quot;]&lt;/code&gt;, which checks all
GitLab CI files (assuming they end in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;) and all Dockerfiles
(which matches &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;Dockerfile.&amp;lt;value&amp;gt;&lt;/code&gt;, or &lt;code&gt;&amp;lt;value&amp;gt;.Dockerfile&lt;/code&gt;).
This could be updated with any other required files.&lt;/p&gt;
&lt;p&gt;The
&lt;a href=&quot;https://docs.renovatebot.com/configuration-options/#matchstrings&quot;&gt;&lt;code&gt;matchStrings&lt;/code&gt;&lt;/a&gt;
property is required and identifies regular expressions to match the
dependencies that the regex manager should manage (in this case container
images). To keep it as flexible as possible it has a &lt;code&gt;depName&lt;/code&gt; capture group
with the dependency name, and a &lt;code&gt;currentValue&lt;/code&gt; capture group with the version.
So, for the image and tag that is &lt;code&gt;(?&amp;lt;depName&amp;gt;[&#92;&#92;S]+):(?&amp;lt;currentValue&amp;gt;[&#92;&#92;S]+)&lt;/code&gt;,
using non-whitespace (&lt;code&gt;&#92;S&lt;/code&gt;) to allow any valid characters for both.&lt;/p&gt;
&lt;p&gt;To constrain the &lt;code&gt;matchStrings&lt;/code&gt; further, the regular expression is updated with
the keywords indicating a container image. For &lt;code&gt;Dockerfile&lt;/code&gt;s, that&#39;s simply
&lt;code&gt;FROM &amp;lt;image_name&amp;gt;&lt;/code&gt;. For &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; files, the following cases are
covered:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &amp;lt;image_name&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &amp;lt;image_name&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &amp;lt;image_name&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note this does not cover all variations of container images in &lt;code&gt;services&lt;/code&gt; (it&#39;s
missing at least two), but includes all that I am currently using. See the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#services&quot;&gt;GitLab &lt;code&gt;services&lt;/code&gt; docs&lt;/a&gt; for
additional details.&lt;/p&gt;
&lt;p&gt;To cover these cases preface the previous &lt;code&gt;matchString&lt;/code&gt; with each of them OR&#39;d
together, which results in:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;matchStrings&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;(?:image:&#92;&#92;s+name:&#92;&#92;s*|image:&#92;&#92;s*|services:&#92;&#92;s+-&#92;&#92;s+|FROM&#92;&#92;s+)(?&amp;lt;depName&gt;[&#92;&#92;S]+):(?&amp;lt;currentValue&gt;[&#92;&#92;S]+)&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href=&quot;https://regex101.com/r/qcgvRM/2&quot;&gt;here&lt;/a&gt; for a full breakdown of this regular
expression, with examples.&lt;/p&gt;
&lt;p&gt;The
&lt;a href=&quot;https://docs.renovatebot.com/configuration-options/#datasourcetemplate&quot;&gt;&lt;code&gt;datasourceTemplate&lt;/code&gt;&lt;/a&gt;
property specifies the data source type, which is &lt;code&gt;docker&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For versioning image tags, specifically trying to override the default &lt;code&gt;docker&lt;/code&gt;
versioning behavior, specify a
&lt;a href=&quot;https://docs.renovatebot.com/configuration-options/#versioningtemplate&quot;&gt;&lt;code&gt;versioningTemplate&lt;/code&gt;&lt;/a&gt;
property telling Renovate how to extract data from the &lt;code&gt;currentValue&lt;/code&gt; (that is,
the image tag). The default behavior is described previously, which is going to
be reversed to track the Alpine versions. Taking the previous &lt;code&gt;python&lt;/code&gt; example,
the tag &lt;code&gt;3.11.4-alpine3.17&lt;/code&gt; has &lt;code&gt;compatibility&lt;/code&gt; = &lt;code&gt;3.11.4-alpine&lt;/code&gt;, &lt;code&gt;major&lt;/code&gt;
version = &lt;code&gt;3&lt;/code&gt;, and &lt;code&gt;minor&lt;/code&gt; version = &lt;code&gt;17&lt;/code&gt;. Using the previous data on different
tags formats gives:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;versioningTemplate&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;regex:^(?&amp;lt;compatibility&gt;[&#92;&#92;S]*&#92;&#92;d+&#92;&#92;.&#92;&#92;d+(?:&#92;&#92;.&#92;&#92;d+)?(?:[&#92;&#92;S]*)?-alpine-?)(?&amp;lt;major&gt;&#92;&#92;d+)&#92;&#92;.(?&amp;lt;minor&gt;&#92;&#92;d+)(?:&#92;&#92;.(?&amp;lt;patch&gt;&#92;&#92;d+))?$&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href=&quot;https://regex101.com/r/Ul5kMB/8&quot;&gt;here&lt;/a&gt; for a full breakdown of this regular
expression, with examples.&lt;/p&gt;
&lt;p&gt;The final Renovate configuration for this regex manager is shown below:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://docs.renovatebot.com/renovate-schema.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;extends&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;config:base&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;docker:enableMajor&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;regexManagers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Manage Alpine OS versions in container image tags&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;fileMatch&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&#92;&#92;.gitlab-ci&#92;&#92;.yml$&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Dockerfile&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;matchStrings&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;(?:image:&#92;&#92;s+name:&#92;&#92;s*|image:&#92;&#92;s*|services:&#92;&#92;s+-&#92;&#92;s+|FROM&#92;&#92;s+)(?&amp;lt;depName&gt;[&#92;&#92;S]+):(?&amp;lt;currentValue&gt;[&#92;&#92;S]+)&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;versioningTemplate&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;regex:^(?&amp;lt;compatibility&gt;[&#92;&#92;S]*&#92;&#92;d+&#92;&#92;.&#92;&#92;d+(?:&#92;&#92;.&#92;&#92;d+)?(?:[&#92;&#92;S]*)?-alpine-?)(?&amp;lt;major&gt;&#92;&#92;d+)&#92;&#92;.(?&amp;lt;minor&gt;&#92;&#92;d+)(?:&#92;&#92;.(?&amp;lt;patch&gt;&#92;&#92;d+))?$&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;datasourceTemplate&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;docker&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-limitations&quot; tabindex=&quot;-1&quot;&gt;The limitations &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/managing-alpine-linux-based-container-images-with-renovate/#the-limitations&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In general, the default &lt;code&gt;docker&lt;/code&gt; versioning and &lt;code&gt;regex&lt;/code&gt; versioning work
independently, and should identify updates for either or both, as applicable.
The one known limitation is when both identify a major update, or both identify
a minor/patch update. This can be seen with images like
&lt;code&gt;python:3.11.3-alpine3.17&lt;/code&gt;, which has a minor version update to Python 3.11.4
and also a &amp;quot;minor&amp;quot; version update to Alpine 3.18 (since major/minor are defined
this way in the custom versioning). So, Renovate sees two &lt;code&gt;python-3.x&lt;/code&gt; updates,
and one of them is ignored (in the Renovate logs you&#39;ll see the message
&amp;quot;Ignoring upgrade collision&amp;quot;). With this example the &lt;code&gt;python:3.11.4-alpine3.17&lt;/code&gt;
update alone is processed, and once it&#39;s merged the &lt;code&gt;python:3.11.4-alpine3.18&lt;/code&gt;
update is processed.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Bin Tester v4.0.0 Released</title>

    <link href="https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/" />
    <updated>2023-05-28T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/</id>
    <content type="html">&lt;p&gt;Bin Tester v4.0.0 was released today with several noteworthy changes, including
changing the function exports, changing the argument to the test functions,
adding TypeScript type declarations, and deprecating support for Node 14 and 19.&lt;/p&gt;
&lt;h2 id=&quot;bin-tester-overview&quot; tabindex=&quot;-1&quot;&gt;Bin Tester overview &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/#bin-tester-overview&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/bin-tester&quot;&gt;Bin Tester&lt;/a&gt; provides a function to
simplify testing of a package&#39;s &lt;code&gt;bin&lt;/code&gt; commands. It spawns a child process,
executes the command in a specific folder with specific arguments/environment
variables, and returns the results - exit code, any &lt;code&gt;Error&lt;/code&gt; thrown, and the full
text of &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;changes-in-v4.0.0&quot; tabindex=&quot;-1&quot;&gt;Changes in v4.0.0 &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/#changes-in-v4.0.0&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are three noteworthy changes in v4.0.0, all of which could be breaking.
Please see
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester/-/releases/4.0.0&quot;&gt;the release notes&lt;/a&gt;
for other minor changes.&lt;/p&gt;
&lt;h3 id=&quot;exported-test-function-and-argument-changes-(breaking)&quot; tabindex=&quot;-1&quot;&gt;Exported test function and argument changes (BREAKING) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/#exported-test-function-and-argument-changes-(breaking)&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Previously the test function used the default export, but it was moved to the
named &lt;code&gt;bin&lt;/code&gt; export to allow for future expansion of the package. The &lt;code&gt;bin&lt;/code&gt;
function also now accepts a single &lt;code&gt;options&lt;/code&gt; object argument with properties for
all of the previous arguments.&lt;/p&gt;
&lt;p&gt;The following is an example test &lt;code&gt;jest&lt;/code&gt; with the previous API. As can be seen,
the number possible of arguments has gotten long over time, which makes it
difficult to specify a subset of the arguments, and unclear which arguments are
actually required for test.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// bin-tester@3.0.1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; bin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;bin-tester&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;should successfully execute and output JSON results to stdout if JSON format specified&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; testArgs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;-f&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; commandName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;my-command&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Previously API was:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//   bin(binArguments, workingDirectory, packageDirectory, environment, commandName, timeout)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;testArgs&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;./&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;./&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; commandName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stderr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stdout&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMatchSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To update for the change in v4.0.0:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;bin&lt;/code&gt; function is now required/imported as a named export.&lt;/li&gt;
&lt;li&gt;The required &lt;code&gt;bin&lt;/code&gt; function arguments should be encapsulated in one &lt;code&gt;options&lt;/code&gt;
object.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These changes are shown in the example below. In this case only the two required
arguments are specified (as opposed to the 5 in the preceding example).&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// bin-tester@4.0.0&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; bin &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;bin-tester&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;should successfully execute and output JSON results to stdout if JSON format specified&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; options &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;binArguments&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;-f&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;my-command&#39;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stderr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;results&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stdout&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMatchSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complete list of properties for the &lt;code&gt;options&lt;/code&gt; object can be found
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/bin-tester#usage&quot;&gt;in the README&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;added-typescript-type-declarations-(breaking)&quot; tabindex=&quot;-1&quot;&gt;Added TypeScript type declarations (BREAKING) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/#added-typescript-type-declarations-(breaking)&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The package now contains built-in TypeScript type declarations, part of a bigger
effort to add types for all of my library-type packages. This is only BREAKING
if you have implemented custom types for this package.&lt;/p&gt;
&lt;h3 id=&quot;deprecated-support-for-node-14-and-19-(breaking)&quot; tabindex=&quot;-1&quot;&gt;Deprecated support for Node 14 and 19 (BREAKING) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/bin-tester-v4.0.0-released/#deprecated-support-for-node-14-and-19-(breaking)&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Support for Node 14 (which went end of life on April 30, 2023) and Node 19
(which goes end of life on June 1, 2023) has been deprecated. The package
continues to be tested against all current and LTS releases
(&lt;code&gt;^16.13.0 || ^18.12.0 || &amp;gt;=20.0.0&lt;/code&gt;). Once Node releases are end of life, any
identified vulnerabilities are not patched, and this is intended to actively
discourage keeping unsupported version of Node in use.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>GitLab CI Pipeline for Eleventy</title>

    <link href="https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/" />
    <updated>2023-04-03T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/</id>
    <content type="html">&lt;p&gt;This post details the GitLab CI pipeline used for this blog, which is built with
&lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt;. It&#39;s based on a collection of GitLab CI
templates that have evolved over several years for my published NPM packages
with a collection of end-to-end tests used for web applications and a few unique
jobs added specifically for Eleventy and Nunjucks templates. It&#39;s meant as an
illustration of a reasonably comprehensive CI pipeline for an Eleventy static
site, maximizing the level of automated testing, leveraging built-in GitLab
capabilities where practical, and optimizing parallelization and pipeline speed.&lt;/p&gt;
&lt;h2 id=&quot;ci-pipeline-philosophy&quot; tabindex=&quot;-1&quot;&gt;CI pipeline philosophy &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#ci-pipeline-philosophy&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The following summarizes the general philosophy that I use for any CI pipeline.
Some pieces are GitLab specific, but most are general platform independent
recommendations based on my experience.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The CI pipeline should execute all checks that are required to confirm that
the application is in an acceptable configuration for deployment. This
includes checking multiple configurations where applicable (for example
light/dark mode, desktop/mobile configurations, OS/platform/framework
versions).&lt;/li&gt;
&lt;li&gt;CI jobs should be deterministic, with job failure meaning a failure to execute
properly or an unacceptable result that should not being deployed. There may
be jobs with interpreted results (for example Lighthouse performance score
changes, dependency vulnerabilities), but passing jobs should mean the results
were successfully generated to be interpreted. Jobs that fail, but allow
failure, resulting in the pipeline passing are a trap to be avoided. This can
eventually leads to not reviewing the job results, assuming the failure cause
is known, and the job failing for another reason goes unnoticed.&lt;/li&gt;
&lt;li&gt;Some might argue that CI pipelines should be highly optimized between what
runs in a merge request and what runs on the primary branch. In general that
is a mistake in GitLab CI. The GitLab merge request widgets are optimized to
show changes since in the context of that merge request, for example, if you
run a Code Quality report you only see the changes in that merge request, not
all issues. If there is no reference job in the default branch, then the all
results are identified as changes. Unless there are long-running jobs that
need to be optimized or other specific drivers, there is benefit to running
consistent pipelines.&lt;/li&gt;
&lt;li&gt;The pipeline should leverage built-in GitLab capabilities where appropriate.
In this pipeline that includes analyses like code quality and some security
related testing. There&#39;s no point in re-inventing the wheel unless there&#39;s a
specific driver.&lt;/li&gt;
&lt;li&gt;The pipeline should be optimized to execute as many jobs as possible in
parallel with clearly defined job dependencies, and should minimize repeating
the same tasks in favor of performing them once and passing the artifacts as
required.&lt;/li&gt;
&lt;li&gt;Jobs with external dependencies (for example dependency vulnerability
scanning) should be run on a regular schedule, even with no commits to the
repository.&lt;/li&gt;
&lt;li&gt;Deployment should be performed via an automated scripted process for
consistency (even if manually initiated).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;gitlab-ci-pipeline-for-eleventy&quot; tabindex=&quot;-1&quot;&gt;GitLab CI pipeline for Eleventy &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#gitlab-ci-pipeline-for-eleventy&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The complete CI pipeline for this site is shown below, ordered by execution
sequence with job dependencies shown.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/pipeline.webp&quot; width=&quot;1593&quot; height=&quot;1039&quot; alt=&quot;GitLab CI Pipeline&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;p&gt;For efficiency, all jobs are defined with explicit &lt;code&gt;needs&lt;/code&gt;, as can be seen in
the preceding example. This ensures that jobs start as early as possible, and
that only the artifacts required for the job are downloaded and extracted
(GitLab downloads the artifacts from all previous jobs unless otherwise
specified).&lt;/p&gt;
&lt;p&gt;There are 28 jobs in the pipeline, which are grouped into the following
functional categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#installing-dependencies&quot;&gt;Installing Dependencies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#language-specific-linting-and-formatting&quot;&gt;Language Specific Linting and Formatting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#linting-tool-configuration-files&quot;&gt;Linting Tool Configuration Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#build-site&quot;&gt;Build Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#code-analysis&quot;&gt;Code Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#end-to-end-testing&quot;&gt;End-to-End Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#software-composition-analysis&quot;&gt;Software Composition Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#static-application-security-testing&quot;&gt;Static Application Security Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#site-deployment&quot;&gt;Site Deployment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;installing-dependencies&quot; tabindex=&quot;-1&quot;&gt;Installing dependencies &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#installing-dependencies&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;npm_install&lt;/code&gt; job installs all npm dependencies and saves then as artifacts.
This allows the install to be performed only once since it is an expensive
operation (in time and I/O) and the dependencies are then made available to any
subsequent jobs that require them.&lt;/p&gt;
&lt;h3 id=&quot;language-specific-linting-and-formatting&quot; tabindex=&quot;-1&quot;&gt;Language specific linting and formatting &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#language-specific-linting-and-formatting&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Linting and format checking is performed for all languages used in this project.
I use a very opinionated linting configurations for all tools to ensure
consistency and maintainability throughout the codebase and to check for common
issues. Development is done in a mix of VSCode and GitLab&#39;s WebIDE, so linting
and formatting is not always checked there. When working VSCode, linting and
format checking is performed is as a git pre-commit hook. The one place where
linting and format checking is always performed is in CI.&lt;/p&gt;
&lt;p&gt;The challenge with the pre-commit hook and CI cases is that the resulting logs
are not reviewed unless there is an error. Operating this was for a long time on
many projects has driven me to update all linting rule configurations to remove
all warnings. Rules are either important enough to be errors or they&#39;re not and
they&#39;re disabled. The errors then fail the pre-commit hook or CI pipeline. In
some cases there are acceptable deviations, but these exceptions are then
disabled in the code with the appropriate rationale.&lt;/p&gt;
&lt;p&gt;The specific linting/formatting jobs and tools are detailed in the table below.
Most of these are probably familiar names, other than maybe djLint. It&#39;s a
Python tool for linting and formatting Nunjucks, Django, Handlebars, and many
others. There&#39;s no other tool I&#39;ve found in that space that comes close.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint_css&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://stylelint.io/&quot;&gt;&lt;code&gt;stylelint&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint_js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://eslint.org/&quot;&gt;&lt;code&gt;eslint&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint_md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/DavidAnson/markdownlint&quot;&gt;&lt;code&gt;markdownlint&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint_nunjucks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.djlint.com/&quot;&gt;&lt;code&gt;djlint&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint_prettier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://prettier.io/&quot;&gt;&lt;code&gt;prettier&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lint_yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/adrienverge/yamllint&quot;&gt;&lt;code&gt;yamllint&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;linting-tool-configuration-files&quot; tabindex=&quot;-1&quot;&gt;Linting tool configuration files &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#linting-tool-configuration-files&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are a few tools used by this project that offer linting tools to validate
their configuration files. These jobs perform those checks to identify any
formatting errors.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;lint_pageanrc&lt;/code&gt; job checks the
&lt;a href=&quot;https://www.npmjs.com/package/pagean&quot;&gt;Pagean&lt;/a&gt; configuration file for validity.
See the &lt;a href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#end-to-end-testing&quot;&gt;End-to-End Testing&lt;/a&gt; section for more details on
Pagean. Formatting errors would also be found when running Pagean, but this
provides a job that explicitly identifies a configuration error, and this can be
run much earlier in the pipeline to fail fast if there is an issue.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;lint_renovate&lt;/code&gt; job checks the &lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt;
configuration file for validity. Renovate is used by this project for automated
dependency updates, is quite powerful/configurable, and is highly recommended.
The Renovate process is run in another project, so an error in the Renovate
configuration would not otherwise be detected in this pipeline.&lt;/p&gt;
&lt;h3 id=&quot;build-site&quot; tabindex=&quot;-1&quot;&gt;Build site &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#build-site&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;build_test_site&lt;/code&gt; job builds the site in a test configuration. This site
undergoes exhaustive end-to-end testing, so some production pieces are excluded
from the test build (for example analytics scripts), and the configuration sets
the &lt;code&gt;pathPrefix&lt;/code&gt; to match the URLs used for that end-to-end testing.&lt;/p&gt;
&lt;h3 id=&quot;code-analysis&quot; tabindex=&quot;-1&quot;&gt;Code analysis &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#code-analysis&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The pipeline includes a couple of general code analysis jobs that don&#39;t fall
into one of the specific categories listed below.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;code_count&lt;/code&gt; job uses &lt;a href=&quot;https://github.com/AlDanial/cloc&quot;&gt;cloc&lt;/a&gt; to count
lines of code by language. This is primarily used to generate a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/metrics_reports.html&quot;&gt;GitLab Metrics report&lt;/a&gt;,
which can then provide a change in code count by language in a merge request.
This is helpful as a check that an unexpected change wasn&#39;t accidentally
committed (for example if a new blog post was added, which should only be a
change in lines of markdown, but the lines of JavaScript changed as well, then
something is wrong).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;code_quality&lt;/code&gt; job run the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html&quot;&gt;GitLab Code Quality&lt;/a&gt;
testing. The results for the entire project can be viewed, as can the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/code_quality.html#view-code-quality-results&quot;&gt;changes in a merge request&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;end-to-end-testing&quot; tabindex=&quot;-1&quot;&gt;End-to-end testing &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#end-to-end-testing&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;End-to-end testing, which involves analysis of the site in a browser, is
performed to assess site performance, accessibility, and other quality control
checks (for example no console errors, no broken links) as outlined below.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/lighthouse&quot;&gt;Lighthouse&lt;/a&gt; is run against one page
to get a representative set of data in both desktop (the &lt;code&gt;lighthouse_desktop&lt;/code&gt;
job) and mobile (the &lt;code&gt;lighthouse_mobile&lt;/code&gt; job) configurations. This data then
populates a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/metrics_reports.html&quot;&gt;GitLab Metrics report&lt;/a&gt;,
which identifies any changes in Lighthouse scores in a merge request. See
&lt;a href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/&quot;&gt;this post&lt;/a&gt;
for more details on setting up Lighthouse and generating a metrics report.&lt;/p&gt;
&lt;p&gt;Accessibility testing is performed using
&lt;a href=&quot;https://www.npmjs.com/package/pa11y-ci&quot;&gt;Pa11y CI&lt;/a&gt; with both light mode (the
&lt;code&gt;pa11y_ci&lt;/code&gt; job) and dark mode (the &lt;code&gt;pa11y_ci_dark&lt;/code&gt; job) themes. Pally CI can be
configured to run with different accessibility test runners, and these jobs run
both the &lt;a href=&quot;https://squizlabs.github.io/HTML_CodeSniffer/&quot;&gt;HTML_CodeSniffer&lt;/a&gt; and
&lt;a href=&quot;https://www.deque.com/axe/&quot;&gt;Axe&lt;/a&gt; runners. There is also accessibility data
provided by Lighthouse, but the Pa11y CI results in general are more
comprehensive (especially when using both runners).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;pagean&lt;/code&gt; job run &lt;a href=&quot;https://www.npmjs.com/package/pagean&quot;&gt;Pagean&lt;/a&gt;, a test
framework I started several years ago with a collection of tests intended to be
run against all pages in a site - broken links, errors written to the console,
horizontal scroll bar, page load time, and others. See
&lt;a href=&quot;https://www.npmjs.com/package/pagean&quot;&gt;the documentation&lt;/a&gt; for complete details.&lt;/p&gt;
&lt;h3 id=&quot;software-composition-analysis&quot; tabindex=&quot;-1&quot;&gt;Software composition analysis &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#software-composition-analysis&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Software Composition Analysis (SCA) involves a set of tools used to manage risks
from a project&#39;s dependencies. This is a broad category that includes tools to
catalog the project dependencies, check those dependencies for known
vulnerabilities or other security risks, check for unused or deprecated
dependencies, etc.&lt;/p&gt;
&lt;p&gt;Two tools are used to check the project&#39;s dependencies for known
vulnerabilities. The &lt;code&gt;owasp_dependency_check&lt;/code&gt; job runs
&lt;a href=&quot;https://owasp.org/www-project-dependency-check/&quot;&gt;OWASP Dependency Check&lt;/a&gt;
against the project. This is a tool maintained by the OWASP foundation, and
checks not only package manifests like a lockfile, it also scans all files in
the project to check for any known vulnerabilities. This allows it to recognize
cases where dependencies are embedded and/or not identified, but also means the
scan is slower because it has to check every file in the project. It uses data
primarily from the U.S.
&lt;a href=&quot;https://nvd.nist.gov/&quot;&gt;National Vulnerability Database (NVD)&lt;/a&gt;, populated from
the Common Vulnerabilities and Exposures (CVE) database, the effective
international standard for identifying vulnerabilities. It also includes data
from other applicable vulnerability databases. The results not only identify any
known vulnerabilities, but also provide background on the vulnerabilities and
potential remediation steps.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;osv_scanner&lt;/code&gt; job runs &lt;a href=&quot;https://github.com/google/osv-scanner&quot;&gt;OSV Scanner&lt;/a&gt;
to check for vulnerabilities identified in a lockfile or other package manifest.
It checks that listing of vulnerabilities against the
&lt;a href=&quot;https://osv.dev/&quot;&gt;Open Source Vulnerabilities (OSV) Database&lt;/a&gt;, maintained by
Google. This includes data from multiple sources, but is focused on open source
vulnerabilities. Because it is using a defined listing of dependencies it runs
quickly. For this specific project, it is functionally no different than running
&lt;code&gt;npm audit&lt;/code&gt; since the GitHub Advisories database is integrated into the OSV
Database, but this project leverages CI templates used for a variety of projects
in different languages, so this provides a broader solution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OSV Scanner is currently undergoing testing here to determine if the data is
reliable enough, and reported in a timely enough fashion, that it could
replace &lt;code&gt;owasp_dependency_check&lt;/code&gt; in projects where a listing of dependencies
is available. So far that has not been shown to be the case. As this is being
written, there is a new vulnerability identified by OWASP Dependency Check
over a week ago that is still not reported in a GitHub Advisory, and therefore
not by OSV Scanner.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://socket.dev/&quot;&gt;Socket&lt;/a&gt; has taken a different approach than many other
tools in the area of dependency security to provide a detailed assessment of the
code for NPM (and other) packages to identify specific capabilities. For
example, it identifies that a package access files on disk, accesses the
network, spawns external processes, sends telemetry, and many other checks.
These actions are expected in some cases, but this provides the insight to
identify cases where it may not be. The &lt;code&gt;socket&lt;/code&gt; job run the
&lt;a href=&quot;https://docs.socket.dev/docs/socket-cli&quot;&gt;Socket CLI&lt;/a&gt; for any merge requests
with dependency changes to assess any policy violations. At this point their
&lt;a href=&quot;https://github.com/apps/socket-security&quot;&gt;GitHub app&lt;/a&gt; is more mature, so this is
a recent addition still undergoing assessment to determine the best way to use
the results in GitLab. It&#39;s a tool worth taking a look at because it really does
provide the most comprehensive assessment of a package&#39;s codebase and potential
issues that I&#39;ve seen.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;lint_lockfile&lt;/code&gt; job runs
&lt;a href=&quot;https://www.npmjs.com/package/lockfile-lint&quot;&gt;npm-lockfile-lint&lt;/a&gt; to check that
packages in the lockfile are properly retrieved from acceptable sources. In this
case it ensures that packages are only pulled from NPM, over HTTPS, that the
integrity field has a SHA512 hash, and that the URL matches the package. This is
done to verify the integrity, fidelity, and vulnerability reporting for all
dependencies (with all of these settings configurable based on project needs).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;depcheck&lt;/code&gt; job runs &lt;a href=&quot;https://www.npmjs.com/package/depcheck&quot;&gt;depcheck&lt;/a&gt; to
identify any project dependencies that are not used or packages that are used
but are not dependencies. It does have some limitations, but finds references
for packages in .js/.ts files, in scripts running packages &lt;code&gt;bin&lt;/code&gt; commands, or in
the configuration files of many major utilities (for example &lt;code&gt;eslint&lt;/code&gt;, &lt;code&gt;jest&lt;/code&gt;,
&lt;code&gt;mocha&lt;/code&gt;, &lt;code&gt;prettier&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;npm_check&lt;/code&gt; job run &lt;a href=&quot;https://www.npmjs.com/package/npm-check&quot;&gt;npm-check&lt;/a&gt; to
identify and packages with updates. While Renovate is used for automated
updates, this is a backup to identify cases where the Renovate job is failing to
make updates.&lt;/p&gt;
&lt;h3 id=&quot;static-application-security-testing&quot; tabindex=&quot;-1&quot;&gt;Static Application Security Testing &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#static-application-security-testing&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Static Application Security Testing (SAST) tools analyze source code to find
coding patterns that represent potential security issues. These analyses all
focus on this project&#39;s code, not any project dependencies. Several SAST
analysis tools are used, as outlined below.&lt;/p&gt;
&lt;p&gt;The
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/sast/&quot;&gt;GitLab SAST template&lt;/a&gt;
includes a series of language-specific SAST analysis tools, with rules
configured to run those applicable to your codebase. In this, with a
Node.js/JavaScript project that includes the &lt;code&gt;nodejs-scan-sast&lt;/code&gt; and
&lt;code&gt;semgrep-sast&lt;/code&gt; jobs.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;secret_detection&lt;/code&gt; job checks for any secrets (for example passwords, access
tokens/keys) committed to the git repository. This job is included from the
&lt;a href=&quot;https://docs.gitlab.com/ee/user/application_security/secret_detection/&quot;&gt;GitLab Security Detection template&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;unicode_bidi_test&lt;/code&gt; job runs
&lt;a href=&quot;https://www.npmjs.com/package/anti-trojan-source&quot;&gt;&lt;code&gt;anti-trojan-source&lt;/code&gt;&lt;/a&gt; to
identify any bidirectional Unicode characters, which could be an indication of a
&lt;a href=&quot;https://trojansource.codes/&quot;&gt;Trojan Source Attack&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;site-deployment&quot; tabindex=&quot;-1&quot;&gt;Site deployment &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-ci-pipeline-for-eleventy/#site-deployment&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As noted in the preceding example, the &lt;code&gt;build_test_site&lt;/code&gt; job builds the site in
a test configuration, so it must be rebuilt with the appropriate settings for
deployment.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;pages&lt;/code&gt; job runs only for commits to the default branch and builds the site
in the production configuration and deploys it to
&lt;a href=&quot;https://docs.gitlab.com/ee/user/project/pages/&quot;&gt;GitLab Pages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;GitLab provides the capability to create a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/review_apps/&quot;&gt;review app&lt;/a&gt; with an instance of an
application with the changes in a merge request. Unfortunately, this capability
is not natively available for GitLab pages. To accomplish this, the
&lt;code&gt;pages_review_app&lt;/code&gt; jobs run on merge request pipelines to leverage some
alternate GitLab capabilities to accomplish the same result. See
&lt;a href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/&quot;&gt;this post&lt;/a&gt;
for details on how to set up a GitLab review app for an Eleventy site.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Passing a GitLab CI Job For a Failing Script</title>

    <link href="https://aarongoldenthal.com/posts/passing-a-gitlab-ci-job-for-a-failing-script/" />
    <updated>2023-02-21T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/passing-a-gitlab-ci-job-for-a-failing-script/</id>
    <content type="html">&lt;p&gt;There are some cases where the expected outcome of a CI job script is failure.
One prominent use case is the testing of tools and container images that are
intended for CI-based analyses. This post details techniques for GitLab CI
scripts that allow the job to pass when the script fails to accurately reflect
the expected result.&lt;/p&gt;
&lt;h2 id=&quot;ci-jobs-expecting-failure&quot; tabindex=&quot;-1&quot;&gt;CI jobs expecting failure &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/passing-a-gitlab-ci-job-for-a-failing-script/#ci-jobs-expecting-failure&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;GitLab CI considers a job successful if the &lt;code&gt;script&lt;/code&gt; completes with an exit code
of 0, and considers it failed with any other exit code. In most cases that logic
is appropriate, but there are some cases where a job is expected to fail. Two
examples seen frequently revolve around CI pipelines for the testing of testing
tools - integration testing of testing tools, and testing container images
intended for use with testing tools.&lt;/p&gt;
&lt;p&gt;To manage these cases, one obvious option for those familiar with GitLab CI is
to use &lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#allow_failure&quot;&gt;&lt;code&gt;allow_failure&lt;/code&gt;&lt;/a&gt;,
which allow failure for any exit code when set to true. This can be enhanced by
using &lt;code&gt;allow_failure&lt;/code&gt; with
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#allow_failureexit_codes&quot;&gt;&lt;code&gt;exit_codes&lt;/code&gt;&lt;/a&gt;,
which accepts an array of exit codes and only those codes allow failure. For
example:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;some_job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;allow_failure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;exit_codes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;123&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;456&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This only allows failure for exit codes 123 and 456, and any others fail the job
and the pipeline. While this allows the pipeline to proceed and pass, it still
leaves the job, and ultimately the pipeline, passing with failure.&lt;/p&gt;
&lt;h2 id=&quot;the-value-of-a-green-pipeline&quot; tabindex=&quot;-1&quot;&gt;The value of a green pipeline &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/passing-a-gitlab-ci-job-for-a-failing-script/#the-value-of-a-green-pipeline&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Everyone strives to have a green pipeline status with all jobs passed as a
definitive indication that all jobs resulted in the expected outcome. In the
case where a job passed with failure, people either don&#39;t, or certainly don&#39;t
want to have to, examine the log for a job on every pipeline to see why it
failed. It could be the expected result of the job using the techniques
previously noted, but could be a variety of other issues - runner failure,
failure to load the container image, script execution error, network error, job
timeout - anything that can cause the job to fail. Even if the job log is
initially examined on each pipeline, it&#39;s
&lt;a href=&quot;https://en.wikipedia.org/wiki/Normalization_of_deviance&quot;&gt;human nature to normalize that result&lt;/a&gt;
and stop looking over time. At that point other problems may occur that go
unnoticed, possibly until merged or released and result in a bigger issue. The
goal should be all jobs passing since any passing with failure case is
ultimately an indeterminate result.&lt;/p&gt;
&lt;h2 id=&quot;passing-a-job-when-the-script-fails&quot; tabindex=&quot;-1&quot;&gt;Passing a job when the script fails &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/passing-a-gitlab-ci-job-for-a-failing-script/#passing-a-job-when-the-script-fails&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The results of a command can be reversed by leveraging the logical And (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;)
and Or (&lt;code&gt;||&lt;/code&gt;) operators.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The And (&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;) operator specifies a command that is executed if preceding
command is successful (exit code 0). So in the example &lt;code&gt;foo &amp;amp;&amp;amp; bar&lt;/code&gt;, the &lt;code&gt;bar&lt;/code&gt;
command only executes if the &lt;code&gt;foo&lt;/code&gt; command passes.&lt;/li&gt;
&lt;li&gt;The Or (&lt;code&gt;||&lt;/code&gt;) operator specifies a command that is executed if preceding
command fails (exit code &amp;gt; 0). So in the example &lt;code&gt;foo || bar&lt;/code&gt;, the &lt;code&gt;bar&lt;/code&gt;
command only execute if the &lt;code&gt;foo&lt;/code&gt; command fails.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Putting all of that together results in the &lt;code&gt;script&lt;/code&gt; below, which in this case
runs &lt;a href=&quot;https://github.com/pa11y/pa11y-ci&quot;&gt;&lt;code&gt;pa11y-ci&lt;/code&gt;&lt;/a&gt;, a CI-based web
accessibility testing tool, against a URL known to have accessibility issues
(so, command is expected to fail):&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;some_job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; pa11y&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; exit 1 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; exit 0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs the &lt;code&gt;pa11y-ci&lt;/code&gt; command. If the command is successful, which in this
case is actually a failure, then it exits &lt;code&gt;1&lt;/code&gt; and the job fails. If the
&lt;code&gt;pa11y-ci&lt;/code&gt; command fails, which is expected, then exit &lt;code&gt;0&lt;/code&gt; and the job succeeds.&lt;/p&gt;
&lt;p&gt;This is a good solution for cases where the script command only fails with one
specific exit code. If there are multiple possible exit codes, then this could
mask an error in the job. In this particular case, &lt;code&gt;pa11y-ci&lt;/code&gt; exits with &lt;code&gt;2&lt;/code&gt; if
any accessibility errors are found (what&#39;s tested for), but exits with &lt;code&gt;1&lt;/code&gt; for
other failures. So, if &lt;code&gt;pa11y-ci&lt;/code&gt; fails to execute properly, this job would pass
and mask the failure - not the desired result.&lt;/p&gt;
&lt;h2 id=&quot;passing-only-for-certain-exit-codes&quot; tabindex=&quot;-1&quot;&gt;Passing only for certain exit codes &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/passing-a-gitlab-ci-job-for-a-failing-script/#passing-only-for-certain-exit-codes&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The preceding technique can be taken a step further by adding logic to
specifically check the exit code from the previous command and take the
appropriate action, as in the following example &lt;code&gt;script&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;some_job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; pa11y&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci &lt;span class=&quot;token important&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; exit 2 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; if &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &quot;$&lt;span class=&quot;token punctuation&quot;&gt;?&lt;/span&gt;&quot; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;eq &quot;2&quot; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;; then exit 0; else exit 1; fi&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs the &lt;code&gt;pa11y-ci&lt;/code&gt; command. If it&#39;s successful, which again is actually a
failure, then with the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; operator the script exits &lt;code&gt;2&lt;/code&gt; and the job fails. In
this case &lt;code&gt;2&lt;/code&gt; was chosen to indicate a passing script to differentiate it from
other errors.&lt;/p&gt;
&lt;p&gt;If the &lt;code&gt;pa11y-ci&lt;/code&gt; command fails, which is expected, then the &lt;code&gt;||&lt;/code&gt; operator runs
the &lt;code&gt;if&lt;/code&gt; command. The exit code from the previous command is represented by
&lt;code&gt;$?&lt;/code&gt;, so if that is equal to 2, which is the expected result (that is,
&lt;code&gt;pa11y-ci&lt;/code&gt; detected an accessibility error), then exit &lt;code&gt;0&lt;/code&gt; and the job passes.
Otherwise, there is a different error so exit 1 and the job fails.&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Setup GitLab Review Apps with Eleventy</title>

    <link href="https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/" />
    <updated>2023-02-09T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://docs.gitlab.com/ee/user/project/pages/#gitlab-pages&quot;&gt;GitLab Pages&lt;/a&gt;
provide an easy means of deploying a site hosted on GitLab, but GitLab does not
provide support for creating Review Apps for a Pages site. This post outlines a
reusable technique to work around that and setup Review Apps with
&lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt; to enable creation of a unique, browsable
instance of a site with the changes in a merge request.&lt;/p&gt;
&lt;h2 id=&quot;overview-of-gitlab-review-apps&quot; tabindex=&quot;-1&quot;&gt;Overview of GitLab review apps &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#overview-of-gitlab-review-apps&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;GitLab &lt;a href=&quot;https://docs.gitlab.com/ee/ci/review_apps/&quot;&gt;Review Apps&lt;/a&gt; allow the
creation of temporary environments deploying the code changes in a merge
request. Once configured, a new environment is created for each merge request,
allowing review of a running version of the application. A link to that
application is provided in the merge request widget to simplify access. That
temporary environment is destroyed after a specified time period, or when the
merge request is merged.&lt;/p&gt;
&lt;p&gt;Currently, GitLab only allows a single Pages site and doesn&#39;t support the
deployment of Review Apps for Pages, although there is an
&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/issues/16907&quot;&gt;open issue&lt;/a&gt; to enable that
capability in the future. Until that is implemented, this technique allows
adding that capability for Eleventy sites.&lt;/p&gt;
&lt;h2 id=&quot;gitlab-pages-setup&quot; tabindex=&quot;-1&quot;&gt;GitLab pages setup &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#gitlab-pages-setup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To start with a common foundation, a job to deploy the site to GitLab Pages is
configured. The requirements to deploy a Pages site are a job in the pipeline
named &lt;code&gt;pages&lt;/code&gt; that saves the site as artifacts in the &lt;code&gt;public/&lt;/code&gt; directory. This
job builds on that with a few additional capabilities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The jobs uses the &lt;code&gt;node:lts-alpine&lt;/code&gt; container image, a lightweight image
capable of running Node projects.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;rules&lt;/code&gt; are set to only deploy the Pages site for pipelines on the
&lt;a href=&quot;https://docs.gitlab.com/ee/user/project/repository/branches/default.html&quot;&gt;default branch&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A variable &lt;code&gt;BUILD_TYPE&lt;/code&gt; is added to denote this is a &lt;code&gt;pages&lt;/code&gt; build. This is
used in the
&lt;a href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#fixing-path-references-with-eleventy-configuration&quot;&gt;eleventy configuration&lt;/a&gt;
to control aspects of the Eleventy build.&lt;/li&gt;
&lt;li&gt;The job sets up a production &lt;code&gt;environment&lt;/code&gt;, which enables the built-in GitLab
features for
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/environments/&quot;&gt;tracking Pages deployments&lt;/a&gt;. The
&lt;code&gt;url&lt;/code&gt; should be set to the URL of the final site.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;script&lt;/code&gt; installs all dependencies and runs the npm &lt;code&gt;build&lt;/code&gt; script,
assuming this builds the Eleventy site (for example &lt;code&gt;npx @11ty/eleventy&lt;/code&gt;).
These steps are included here to have a complete job definition, but depending
on the pipeline those actions could be done in another job and its artifacts
used in the &lt;code&gt;pages&lt;/code&gt; job.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The complete &lt;code&gt;pages&lt;/code&gt; job is shown below.&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;lts&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;BUILD_TYPE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pages
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
    &lt;span class=&quot;token key atrule&quot;&gt;deployment_tier&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
    &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//&amp;lt;site_url&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;/
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm ci
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; public&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;review-apps-setup&quot; tabindex=&quot;-1&quot;&gt;Review apps setup &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#review-apps-setup&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are several steps to configure Review Apps, including enabling Review Apps
for the project, setting up the CI job, and some Eleventy configuration updates.&lt;/p&gt;
&lt;h3 id=&quot;enabling-review-apps&quot; tabindex=&quot;-1&quot;&gt;Enabling review apps &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#enabling-review-apps&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To configure Review Apps they must be enabled for the project, which can be done
per the instructions in the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/review_apps/#enable-review-app-button&quot;&gt;GitLab documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-job-artifacts-workaround-for-review-apps&quot; tabindex=&quot;-1&quot;&gt;The job artifacts workaround for review apps &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#the-job-artifacts-workaround-for-review-apps&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Without a dedicated means of deploying a Review App for a GitLab Pages site, the
workaround used here relies on the fact that CI job artifacts are actually
served by the GitLab Pages server with a URL following the format
&lt;code&gt;https://&amp;lt;namespace&amp;gt;.&amp;lt;pages_domain&amp;gt;/-/&amp;lt;project&amp;gt;/-/jobs/&amp;lt;job_id&amp;gt;/artifacts/&amp;lt;file path&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As an example, for this site&#39;s
&lt;a href=&quot;https://gitlab.com/aarongoldenthal/aarongoldenthal/&quot;&gt;GitLab project&lt;/a&gt; that URL
would be something like
&lt;code&gt;https://aarongoldenthal.gitlab.io/-/aarongoldenthal/-/jobs/1234567890/artifacts/public/index.html&lt;/code&gt;
(based on the job ID).&lt;/p&gt;
&lt;p&gt;For an Eleventy site, the initial URL should be the &lt;code&gt;index.html&lt;/code&gt; file in the
project root directory. For consistency with the &lt;code&gt;pages&lt;/code&gt; job, it&#39;s assumed that
the project is in the &lt;code&gt;public/&lt;/code&gt; directory, so the &lt;code&gt;&amp;lt;file path&amp;gt;&lt;/code&gt; should be
&lt;code&gt;public/index.html&lt;/code&gt;. The other parameters - &lt;code&gt;&amp;lt;namespace&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;pages_domain&amp;gt;&lt;/code&gt;,
&lt;code&gt;&amp;lt;project&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;job_id&amp;gt;&lt;/code&gt; - can all be taken from
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/predefined_variables.html&quot;&gt;GitLab predefined variables&lt;/a&gt;,
which provides the most generic job definition allowing it to be re-used in
multiple projects on either gitlab.com or a self-hosted GitLab instance.&lt;/p&gt;
&lt;p&gt;The differences from the &lt;code&gt;pages&lt;/code&gt; job are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;rules&lt;/code&gt; are set to only run on
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/pipelines/merge_request_pipelines.html&quot;&gt;merge request pipelines&lt;/a&gt;.
If merge request pipelines are not being used, details are also provided below
for using branch pipelines instead.&lt;/li&gt;
&lt;li&gt;For a Review App, a unique &lt;code&gt;environment:name&lt;/code&gt; must be set up. In this case the
&lt;code&gt;CI_COMMIT_REF_SLUG&lt;/code&gt; is used, which is the branch name. This allows multiple
simultaneous Review Apps, but only one per branch. The &lt;code&gt;url&lt;/code&gt; is set to the
format described previously.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The complete &lt;code&gt;pages_review_app&lt;/code&gt; job is shown below.&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;pages_review_app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;lts&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# If not using MR pipelines this can be set to run on branch pipelines&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# that are not the default branch with:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# if: $CI_COMMIT_BRANCH &amp;amp;&amp;amp; $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_MERGE_REQUEST_ID
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;BUILD_TYPE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pages
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; review/$CI_COMMIT_REF_SLUG
    &lt;span class=&quot;token key atrule&quot;&gt;deployment_tier&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; testing
    &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//$CI_PROJECT_ROOT_NAMESPACE.$CI_PAGES_DOMAIN/&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;/$CI_PROJECT_NAME/&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;/jobs/$CI_JOB_ID/artifacts/public/index.html
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm ci
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; public&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point running a merge request pipeline exposes a Review App, which opens
the home page, but there are two issues that still need to be addressed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The home page opens properly with the Review App, but any root-relative path
references are incorrect. They are pointing to locations relative to &lt;code&gt;/&lt;/code&gt;, but
with this URL format the &lt;code&gt;/&lt;/code&gt; directory is actually the &lt;code&gt;public/&lt;/code&gt; directory,
which is nested 7 directories deep. This affects any root-relative paths in
&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; or other tags.&lt;/li&gt;
&lt;li&gt;Links between pages point to directories, expecting the web server to serve
default pages. In the case of artifacts this does not occur, so a 404 response
is returned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both of these issues are addressed in the following sections.&lt;/p&gt;
&lt;h3 id=&quot;fixing-path-references-with-eleventy-configuration&quot; tabindex=&quot;-1&quot;&gt;Fixing path references with eleventy configuration &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#fixing-path-references-with-eleventy-configuration&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;To resolve the root-relative path issues, the Eleventy configuration needs to be
updated to dynamically set the &lt;code&gt;pathPrefix&lt;/code&gt; property. This updates all of the
URLs to reflect the site being deployed in a sub-directory. For test or
production (i.e GitLab Pages) builds, the default &lt;code&gt;/&lt;/code&gt; path is used, but it&#39;s set
to the appropriate directory for Review Apps.&lt;/p&gt;
&lt;p&gt;To simplify working with
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/variables/predefined_variables.html&quot;&gt;GitLab predefined variables&lt;/a&gt;
the &lt;a href=&quot;https://www.npmjs.com/package/gitlab-ci-env&quot;&gt;&lt;code&gt;gitlab-ci-env&lt;/code&gt;&lt;/a&gt; package is
used in this example. This package returns an object with all GitLab predefined
variable values, hierarchically organized by context, which can simplify cases
where a lot of predefined variables are used. If this is not desired, see the
package documentation for a complete mapping of properties to the actual
predefined environment variable names.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;buildType&lt;/code&gt; uses the previously specified &lt;code&gt;BUILD_TYPE&lt;/code&gt; variable. If this is
not specified, it defaults to &lt;code&gt;test&lt;/code&gt;, intended to be a configuration used for
other testing. The &lt;code&gt;isReviewApp&lt;/code&gt; value is set to true if the build is run in CI
in a merge request pipeline, which matches the preceding CI job definition. In
this case these values are only used to set the &lt;code&gt;pathPrefix&lt;/code&gt;, but could be
exposed for use in other places (for example excluding analytics code in Review
Apps).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;pathPrefix&lt;/code&gt; is finally set using these two values. If the &lt;code&gt;buildType&lt;/code&gt; is
not &lt;code&gt;pages&lt;/code&gt;, or if &lt;code&gt;isReviewApp&lt;/code&gt; is false, the default path is used. Otherwise,
this is a build for a Review App and the path prefix as specified in the
&lt;code&gt;pages_review_app&lt;/code&gt; job is used. The &lt;code&gt;pathPrefix&lt;/code&gt; finally needs to be specified
in the object returned from the Eleventy configuration file.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As noted previously, feature branch pipelines could be used instead of merge
request pipelines. In that case the logic for &lt;code&gt;isReviewApp&lt;/code&gt; should be updated
to match the &lt;code&gt;pages_review_app&lt;/code&gt; rules.&lt;/p&gt;
&lt;p&gt;In this case the &lt;code&gt;output&lt;/code&gt; directory is set to &lt;code&gt;public&lt;/code&gt; as noted in the
preceding CI jobs. This can be set to other values, but the resulting site
output directory must be copied to &lt;code&gt;public&lt;/code&gt; in both Pages CI jobs to be
collected as CI artifacts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The complete Eleventy configuration updates are shown below.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; gitlabEnv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;gitlab-ci-env&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; defaultBuildType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;test&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; defaultPathPrefix &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; buildType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BUILD_TYPE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; defaultBuildType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; isReviewApp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; gitlabEnv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isCI &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; gitlabEnv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mergeRequest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pathPrefix &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
    buildType &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pages&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; isReviewApp
        &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/-/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;gitlabEnv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;project&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/-/jobs/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;gitlabEnv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/artifacts/public/&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; defaultPathPrefix&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        pathPrefix&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;public&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;fixing-links-within-the-site&quot; tabindex=&quot;-1&quot;&gt;Fixing links within the site &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#fixing-links-within-the-site&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The last issue to resolve is that links within the site point to directories,
and GitLab does not serve the default &lt;code&gt;index.html&lt;/code&gt; files. To resolve this, the
output HTML files from the Eleventy build are post-processed to update the
links.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://man7.org/linux/man-pages/man1/find.1.html&quot;&gt;&lt;code&gt;find&lt;/code&gt;&lt;/a&gt; command is used
to search the &lt;code&gt;./public&lt;/code&gt; directory (&lt;code&gt;find ./public&lt;/code&gt;) for any files (&lt;code&gt;-type f&lt;/code&gt;)
whose name matches &lt;code&gt;*.html&lt;/code&gt; (&lt;code&gt;-name &amp;quot;*.html&amp;quot;&lt;/code&gt;). Then a command is executed on
each file (&lt;code&gt;-exec &amp;lt;command&amp;gt; &amp;quot;{}&amp;quot; +&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.gnu.org/software/sed/manual/sed.html&quot;&gt;&lt;code&gt;sed&lt;/code&gt;&lt;/a&gt; command is used to
update the links. The &lt;code&gt;-i&lt;/code&gt; argument makes the changes to the files in place. The
&lt;code&gt;-E&lt;/code&gt; argument allows the use of extended regular expressions, which provides
more consistent behavior of &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;+&lt;/code&gt; (in this case they must be escaped to be
literal values, otherwise they behave as special characters). The regular
expression finds any &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; element with an &lt;code&gt;href&lt;/code&gt; value with a root-relative
path (that is, starting with &lt;code&gt;/&lt;/code&gt;), which includes the &lt;code&gt;public&lt;/code&gt; directory, and an
optional subdirectory. It also captures any other attributes preceding or
following the &lt;code&gt;href&lt;/code&gt; attribute. The &lt;code&gt;sed&lt;/code&gt; command then inserts &lt;code&gt;index.html&lt;/code&gt;
before the &lt;code&gt;href&lt;/code&gt; closing &lt;code&gt;&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For a more detailed explanation of the complete regular expression a great
tool is &lt;a href=&quot;https://regex101.com/&quot;&gt;regex101&lt;/a&gt;, which breaks it down in detail and
allows interactive testing. See &lt;a href=&quot;https://regex101.com/r/EjpdeB/1&quot;&gt;this link&lt;/a&gt;
for this specific regular expression and some examples.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This command is run in &lt;code&gt;after_script&lt;/code&gt; so that the same &lt;code&gt;script&lt;/code&gt; can be re-used
for all Pages jobs.&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;pages_review_app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;after_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      find ./public -type f -name &quot;*.html&quot; -exec
      sed -i -E &#39;s/(&amp;lt;a[^&gt;]*href=&quot;&#92;/.+&#92;/public&#92;/(.*&#92;/)?)(&quot;[^&gt;]*&gt;)/&#92;1index.html&#92;3/&#39; &quot;{}&quot; +&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;refactoring-the-pages-jobs&quot; tabindex=&quot;-1&quot;&gt;Refactoring the pages jobs &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/setup-gitlab-review-apps-with-eleventy/#refactoring-the-pages-jobs&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Looking at both complete jobs there are a lot of common elements, so the final
jobs below extract those common components to a template &lt;code&gt;.pages&lt;/code&gt; which is then
&lt;code&gt;extend&lt;/code&gt;ed for both the &lt;code&gt;pages&lt;/code&gt; and &lt;code&gt;pages_review_app&lt;/code&gt; jobs.&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;.pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;lts&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;BUILD_TYPE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pages
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm ci
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm run build
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; public

&lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .pages
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
    &lt;span class=&quot;token key atrule&quot;&gt;deployment_tier&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production
    &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//&amp;lt;site_url&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;/

&lt;span class=&quot;token key atrule&quot;&gt;pages_review_app&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .pages
  &lt;span class=&quot;token key atrule&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $CI_MERGE_REQUEST_ID
  &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; review/$CI_COMMIT_REF_SLUG
    &lt;span class=&quot;token key atrule&quot;&gt;deployment_tier&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; testing
    &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//$CI_PROJECT_ROOT_NAMESPACE.$CI_PAGES_DOMAIN/&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;/$CI_PROJECT_NAME/&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;/jobs/$CI_JOB_ID/artifacts/public/index.html
  &lt;span class=&quot;token key atrule&quot;&gt;after_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      find ./public  -type f -name &quot;*.html&quot; -exec sed -i -E
      &#39;s/(&amp;lt;a[^&gt;]*href=&quot;&#92;/.+&#92;/public&#92;/(.*&#92;/)?)(&quot;[^&gt;]*&gt;)/&#92;1index.html&#92;3/&#39; &quot;{}&quot; +&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
</content>
    </entry>
    
    <entry>
    <title>GitLab Releaser v5.0.0 Released</title>

    <link href="https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/" />
    <updated>2023-01-30T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/</id>
    <content type="html">&lt;p&gt;GitLab Releaser v5.0.0 was released today with several noteworthy changes,
including failing if an empty release description is pulled from the CHANGELOG
and a new CLI option to specify the CHANGELOG path.&lt;/p&gt;
&lt;h2 id=&quot;gitlab-releaser-overview&quot; tabindex=&quot;-1&quot;&gt;GitLab Releaser overview &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/#gitlab-releaser-overview&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/gitlab-releaser&quot;&gt;GitLab Releaser&lt;/a&gt; takes a JSON
file with details for a release and generates a shell script to leverage
GitLab&#39;s &lt;a href=&quot;https://gitlab.com/gitlab-org/release-cli/&quot;&gt;&lt;code&gt;release-cli&lt;/code&gt;&lt;/a&gt; application
to create a release in the GitLab project. It can be configured to pull the
release name and description (release notes) from the CHANGELOG. GitLab&#39;s
&lt;code&gt;release-cli&lt;/code&gt; application is used to simplify variable expansion, and with the
expectation that it will be
&lt;a href=&quot;https://gitlab.com/gitlab-org/release-cli/-/issues/50&quot;&gt;updated to accept this JSON format&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;changes-in-v5.0.0&quot; tabindex=&quot;-1&quot;&gt;Changes in v5.0.0 &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/#changes-in-v5.0.0&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There are three noteworthy changes in v5.0.0, two of which could be breaking.
Please see
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/gitlab-releaser/-/releases/5.0.0&quot;&gt;the release&lt;/a&gt;
for other minor changes.&lt;/p&gt;
&lt;h3 id=&quot;fail-on-empty-changelog-description-(breaking)&quot; tabindex=&quot;-1&quot;&gt;Fail on empty CHANGELOG description (BREAKING) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/#fail-on-empty-changelog-description-(breaking)&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Previously, if the release description was taken from the CHANGELOG and GitLab
Releaser found an empty description, that was used when creating the release,
resulting in no release notes. History has shown that when this occurs it&#39;s
almost always a formatting error in the CHANGELOG, something like:&lt;/p&gt;
&lt;pre class=&quot;language-md&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; v2.1.0&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; Changed&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Update a great feature to be even better...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, when release &lt;code&gt;4.0.4&lt;/code&gt; is found, GitLab Releaser recognizes that
releases use a &lt;code&gt;##&lt;/code&gt; header, so the next release is assumed to start at
&lt;code&gt;## Changed&lt;/code&gt;, and an empty description is returned. The correct CHANGELOG for
this example is:&lt;/p&gt;
&lt;pre class=&quot;language-md&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; v2.1.0&lt;/span&gt;

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; Changed&lt;/span&gt;

&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; Update a great feature to be even better...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the previous behavior the release was created, and the release notification
sent out, with no release notes. To avoid this, the default behavior was changed
to note the error and fail if this occurs.&lt;/p&gt;
&lt;p&gt;If the previous behavior is desired, there is a new CLI option
&lt;code&gt;--allow-empty-changelog-desc&lt;/code&gt; (&lt;code&gt;-a&lt;/code&gt;) to allow an empty CHANGELOG description.&lt;/p&gt;
&lt;h3 id=&quot;specifying-changelog-path-or-file&quot; tabindex=&quot;-1&quot;&gt;Specifying CHANGELOG path or file &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/#specifying-changelog-path-or-file&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Previously, if the release name or description was taken from the CHANGELOG,
GitLab Releaser assumed the CHANGELOG was located in the project root. While
this is frequently the case, it is not always. There are also cases where the
logic to select the CHANGELOG can fail or select the wrong file, especially if
there are multiple CHANGELOG files or it has an atypical name.&lt;/p&gt;
&lt;p&gt;There is now a CLI option &lt;code&gt;--changelog&lt;/code&gt; (&lt;code&gt;-c&lt;/code&gt;) to be able to specify either the
path to the CHANGELOG using the existing logic to locate the file, or specify
the actual path.&lt;/p&gt;
&lt;h3 id=&quot;programmatic-api-change-(breaking)&quot; tabindex=&quot;-1&quot;&gt;Programmatic API change (BREAKING) &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/gitlab-releaser-v5.0.0-released/#programmatic-api-change-(breaking)&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;With multiple new options added, the &lt;code&gt;getReleaseCliCommand&lt;/code&gt; function was
refactored to accept an &lt;code&gt;options&lt;/code&gt; object with all CLI options. This is only an
impact if that function is used programmatically (which is atypical).&lt;/p&gt;
</content>
    </entry>
    
    <entry>
    <title>Running Lighthouse in GitLab CI with Metrics Report</title>

    <link href="https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/" />
    <updated>2023-01-21T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/</id>
    <content type="html">&lt;p&gt;Google Chrome&#39;s Lighthouse tool is a great resource in the browser and has
become the standard for basic performance and best-practice metrics on websites.
While useful in the browser, a good continuous integration (CI) pipeline
includes all the testing practical to identify any issues as early as possible.
To that end, this post details how to run Lighthouse via the CLI in GitLab CI
and collect a GitLab metrics report so any changes are reported in merge
requests.&lt;/p&gt;
&lt;h2 id=&quot;lighthouse-versus-lighthouse-ci&quot; tabindex=&quot;-1&quot;&gt;Lighthouse versus Lighthouse CI &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#lighthouse-versus-lighthouse-ci&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Lighthouse can be used to analyze a single URL and provide a report with the
results, similar to those obtained when running it in a browser.
&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot;&gt;Lighthouse CI&lt;/a&gt; provides a
wrapper around Lighthouse to be able to run analyses against multiple URLs, run
with multiple retries for each page, set specific thresholds to fail the tests,
track metrics for a URL over time, and more. This also requires uploading the
results to a
&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/getting-started.md#the-lighthouse-ci-server&quot;&gt;Lighthouse CI server&lt;/a&gt;
(at least to track results over time).&lt;/p&gt;
&lt;p&gt;The goals for this example, which is based on analysis of a static site (like
this one), are to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run Lighthouse against a representative page to identify changes and provide
an HTML report to view the detailed results. This should be done for both a
mobile and desktop browser configuration.&lt;/li&gt;
&lt;li&gt;Make as much data as practical available through the GitLab merge request UI.
This is intended to reinforce the existing philosophy in GitLab using merge
request widgets as the portal to all merge request related data, and minimize
the cases where you have to navigate elsewhere to view data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Given those goals, Lighthouse is used in this example. In general, Lighthouse CI
could be used instead, but requires some changes to CLI commands, configuration
files, and a Lighthouse CI server (which is then another place to go to view
results). See the
&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/getting-started.md&quot;&gt;Lighthouse CI documentation&lt;/a&gt;
for additional details.&lt;/p&gt;
&lt;h2 id=&quot;lighthouse-container-image&quot; tabindex=&quot;-1&quot;&gt;Lighthouse container image &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#lighthouse-container-image&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The desire in this case, as with most GitLab CI jobs, is to run Lighthouse in a
container (for so many reason it needs to be an post unto itself). While
&lt;a href=&quot;https://www.npmjs.com/package/lighthouse&quot;&gt;Lighthouse&lt;/a&gt; and
&lt;a href=&quot;https://www.npmjs.com/package/@lhci/cli&quot;&gt;Lighthouse CI&lt;/a&gt; are both available as
npm packages which can be run from the CLI, they do not install a version of
Chromium, unlike some other Chromium-based analysis tools, instead requiring it
to be installed on the platform (that is, in the container).&lt;/p&gt;
&lt;p&gt;While a GitLab CI job could be setup to install all required dependencies on
each execution, this can be a lengthy process, so to reduce pipeline execution
times a dedicated container with all dependencies pre-installed is preferred.
Since there were none easily found with the desired capabilities, I created a
new &lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse&quot;&gt;Lighthouse container image&lt;/a&gt;
project to build and publish an image. This image is based on the Node 18 Debian
Bullseye Slim image with the following dependencies installed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chromium&lt;/code&gt;: The latest Chromium release for Debian Bullseye.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/lighthouse&quot;&gt;&lt;code&gt;lighthouse&lt;/code&gt;&lt;/a&gt;: The npm package for
running Lighthouse via the CLI.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@lhci/cli&quot;&gt;&lt;code&gt;@lhci/cli&lt;/code&gt;&lt;/a&gt;: The npm package for
running Lighthouse CI via the CLI.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/serve&quot;&gt;&lt;code&gt;serve&lt;/code&gt;&lt;/a&gt;: A CLI-based static web server.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/wait-on&quot;&gt;&lt;code&gt;wait-on&lt;/code&gt;&lt;/a&gt;: A CLI-based tool to wait
for a web server (or other resources) to become available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This image also runs by default as a non-privileged user, which is not only a
general best-practice, running as root requires running Chromium without a
sandbox (that is, with the &lt;code&gt;--no-sandbox&lt;/code&gt; argument). While running in a
container itself minimizes the risk of running without a sandbox, it&#39;s certainly
desirable to have when possible.&lt;/p&gt;
&lt;p&gt;The container images for this project are hosted on gitlab.com in the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/container_registry&quot;&gt;project&#39;s container registry&lt;/a&gt;
as &lt;code&gt;registry.gitlab.com/gitlab-ci-utils/lighthouse&lt;/code&gt;. The &lt;code&gt;latest&lt;/code&gt; tag is
recommended, but others are available. See the
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/&quot;&gt;project documentation&lt;/a&gt; for
additional details on the project and the full
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils/lighthouse/-/blob/master/Dockerfile&quot;&gt;Dockerfile&lt;/a&gt;,
if interested.&lt;/p&gt;
&lt;h2 id=&quot;running-lighthouse&quot; tabindex=&quot;-1&quot;&gt;Running Lighthouse &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#running-lighthouse&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To run Lighthouse, the previously detailed container image is used for the CI
job. The &lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#needs&quot;&gt;&lt;code&gt;needs&lt;/code&gt;&lt;/a&gt; property should be
set to any job or jobs required to build the site to be tested, which then
trigger the Lighthouse job as soon as any required artifacts are available. As
noted previously, a goal is to run Lighthouse in both a mobile and desktop
configuration, so a &lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#variables&quot;&gt;&lt;code&gt;variable&lt;/code&gt;&lt;/a&gt;
is set to specify the form factor, which is used in the job script and when
&lt;a href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#generating-a-metrics-report&quot;&gt;generating the metrics report&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/lighthouse&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; test
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; job_that_builds_site
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;FORM_FACTOR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;mobile&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page to be tested is served by a static server running in the container.
This is run in
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#before_script&quot;&gt;&lt;code&gt;before_script&lt;/code&gt;&lt;/a&gt; to optimize
the ability to re-use this job across projects. If it is used for a different
web application needing a different server, only the &lt;code&gt;before_script&lt;/code&gt; has to be
overridden, and only with the script to start the server. In this case &lt;code&gt;serve&lt;/code&gt;
is used since it is installed in the container image, and it is passed the
directory containing the site. The output from &lt;code&gt;serve&lt;/code&gt;, both stdout and stderr,
are piped to null to avoid polluting the job log with
&lt;a href=&quot;https://stackoverflow.com/questions/10508843/what-is-dev-null-21&quot;&gt;&lt;code&gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/a&gt;.
Finally &lt;code&gt;wait-on&lt;/code&gt; in called with the URL to be tested, which causes the script
to wait until the server is running and successfully serves the page.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; serve ./site_folder/ &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; /dev/null 2&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;&amp;amp;1&lt;/span&gt; &amp;amp; wait&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;on http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;3000/
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#script&quot;&gt;&lt;code&gt;script&lt;/code&gt;&lt;/a&gt; runs &lt;code&gt;lighthouse&lt;/code&gt;,
formatted as a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/script.html#split-long-commands&quot;&gt;multi-line block&lt;/a&gt;
for readability, and passes the URL to be tested and the following test options
as &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse#cli-options&quot;&gt;CLI arguments&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run in a headless browser.&lt;/li&gt;
&lt;li&gt;Test only a subset of the Lighthouse categories.&lt;/li&gt;
&lt;li&gt;Output both a JSON and HTML report. The HTML report provides a more convenient
way to view the full results. The JSON report is used to
&lt;a href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#generating-a-metrics-report&quot;&gt;extract the scores to a metrics report&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Specify the root name of the reports via &lt;code&gt;--output-path&lt;/code&gt;, using the
&lt;code&gt;FORM_FACTOR&lt;/code&gt; variable declared previously. This is appended with
&amp;quot;.report.&amp;lt;format&amp;gt;&amp;quot;, so the final JSON report name, for example, is
&lt;code&gt;lighthouse-mobile.report.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;$LIGHTHOUSE_ARGUMENTS&lt;/code&gt; variable is included to allow for adding CLI
arguments without having to override the script (although in this job it is
empty).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&#39;re running this on a gitlab.com shared runner, the &lt;code&gt;--no-sandbox&lt;/code&gt;
argument should not be required. If this is run on a different runner, or
sandbox errors are encountered, the &lt;code&gt;--chrome-flags&lt;/code&gt; argument can be updated
to &lt;code&gt;--chrome-flags=&amp;quot;--headless --no-sandbox&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      lighthouse http://localhost:3000/
      --chrome-flags=&quot;--headless&quot;
      --only-categories=&quot;accessibility,best-practices,performance,seo&quot;
      --output=&quot;json,html&quot;
      --output-path=&quot;lighthouse-$FORM_FACTOR&quot;
      $LIGHTHOUSE_ARGUMENTS&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If desired, some of this configuration could be specified via a Lighthouse
&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse/blob/main/docs/configuration.md&quot;&gt;configuration file&lt;/a&gt;.
Since that can be defined as a JavaScript module, the variable &lt;code&gt;FORM_FACTOR&lt;/code&gt;
would be available there as well to update the configuration.&lt;/p&gt;
&lt;p&gt;Lastly, the HTML and JSON Lighthouse reports are saved as
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#artifacts&quot;&gt;&lt;code&gt;artifacts&lt;/code&gt;&lt;/a&gt;. In addition,
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#artifactswhen&quot;&gt;&lt;code&gt;when: always&lt;/code&gt;&lt;/a&gt; is set to
ensure that any artifacts are saved even on job failure.&lt;/p&gt;
&lt;p&gt;Putting all of that together results in the following job to run Lighthouse
testing against a mobile configuration.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/lighthouse&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; test
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; job_that_builds_site
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;FORM_FACTOR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;mobile&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; serve ./site_folder/ &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; /dev/null 2&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;&amp;amp;1&lt;/span&gt; &amp;amp; wait&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;on http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;3000/
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      lighthouse http://localhost:3000
      --chrome-flags=&quot;--headless&quot;
      --only-categories=&quot;accessibility,best-practices,performance,seo&quot;
      --output=&quot;json,html&quot;
      --output-path=&quot;lighthouse-$FORM_FACTOR&quot;
      $LIGHTHOUSE_ARGUMENTS&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$FORM_FACTOR.report.html
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;$FORM_FACTOR.report.json&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run Lighthouse testing against a desktop configuration, the previous
&lt;code&gt;lighthouse_mobile&lt;/code&gt; job can be
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#extends&quot;&gt;extended&lt;/a&gt;, and minimal additional
overrides are required based on the that job&#39;s definition.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change the &lt;code&gt;FORM_FACTOR&lt;/code&gt; variable to update the report names.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;--preset=desktop&lt;/code&gt; CLI argument via the &lt;code&gt;LIGHTHOUSE_ARGUMENTS&lt;/code&gt;
variable, which updates multiple Lighthouse settings (for example
&lt;code&gt;--form-factor&lt;/code&gt;, &lt;code&gt;--screenEmulation&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_desktop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse_mobile
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;LIGHTHOUSE_ARGUMENTS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;--preset=desktop&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;FORM_FACTOR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;desktop&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;exposing-artifacts-in-merge-requests&quot; tabindex=&quot;-1&quot;&gt;Exposing artifacts in merge requests &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#exposing-artifacts-in-merge-requests&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;GitLab provides the capability to specify job artifacts that are exposed with
links directly in the merge request UI, which avoids having to go to the job to
view those artifacts. This is accomplished by adding the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#artifactsexpose_as&quot;&gt;&lt;code&gt;expose_as&lt;/code&gt;&lt;/a&gt; property
to the job &lt;code&gt;artifacts&lt;/code&gt; with the name to display with the artifacts.&lt;/p&gt;
&lt;p&gt;Unfortunately, there&#39;s an open
&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/issues/37129&quot;&gt;issue in GitLab&lt;/a&gt; where the
&lt;code&gt;expose_as&lt;/code&gt; capability does not work when there are variables in the
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#artifactspaths&quot;&gt;&lt;code&gt;artifacts:paths&lt;/code&gt;&lt;/a&gt; names,
which has been used previously. In this case the artifacts are properly saved,
but they&#39;re not exposed in the merge request UI. So, to have the artifacts
exposed the &lt;code&gt;FORM_FACTOR&lt;/code&gt; variable needs to be backed out of &lt;code&gt;artifacts:paths&lt;/code&gt;
and updated to list the complete names. This has to be repeated in both jobs
since the &lt;code&gt;FORM_FACTOR&lt;/code&gt; variable is different in each case to uniquely identify
the reports. Note that for the &lt;code&gt;lighthouse_desktop&lt;/code&gt; job only the
&lt;code&gt;artifacts:paths&lt;/code&gt; must be specified since the other values are still inherited
from the &lt;code&gt;lighthouse_mobile&lt;/code&gt; job.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always
    &lt;span class=&quot;token key atrule&quot;&gt;expose_as&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Lighthouse Report&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mobile.report.html
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mobile.report.json

&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_desktop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;desktop.report.html
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;desktop.report.json&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With these changes made, once the CI pipeline is complete, the Lighthouse report
artifacts are exposed in the merge request UI as shown in the screenshot below.
Note that the individual jobs are listed as well to identify the source.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/lighthouse-expose-artifacts.webp&quot; width=&quot;1196&quot; height=&quot;286&quot; alt=&quot;Expose artifacts in a merge request&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;h3 id=&quot;generating-a-metrics-report&quot; tabindex=&quot;-1&quot;&gt;Generating a metrics report &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#generating-a-metrics-report&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/metrics_reports.html&quot;&gt;metrics reports&lt;/a&gt;
are a GitLab Premium feature and not available in GitLab Free. If you&#39;re using
GitLab for open source work see the
&lt;a href=&quot;https://about.gitlab.com/solutions/open-source/join/&quot;&gt;GitLab Open Source Program&lt;/a&gt;
for details on how to register to get free GitLab Ultimate licenses for those
projects to unlock this feature and many others.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The last step is to generate a GitLab
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/testing/metrics_reports.html&quot;&gt;metrics report&lt;/a&gt; for
the results. A metrics report is simply a file of key/value pairs, each on a new
line, conforming to the &lt;a href=&quot;https://openmetrics.io/&quot;&gt;OpenMetrics&lt;/a&gt; format. GitLab
ingests the report and identifies any changes in the merge request UI - new
metrics, removed metrics, or metrics with changed values. As with all merge
request reporting, GitLab reports changes from the source branch in that merge
request, which allows easy identification when any of the scores change. In this
case the goal is to have metrics for the score in each Lighthouse category and
form factor being analyzed, so the metric keys take the form
&lt;code&gt;lighthouse{category=&amp;quot;performance&amp;quot;,form_factor=&amp;quot;mobile&amp;quot;}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The results are taken from the Lighthouse JSON report. There&#39;s a lot of data in
the JSON report, the full details of which are beyond the scope of this post,
but the desired data in this case is summarized below. At the root level there&#39;s
a &lt;code&gt;categories&lt;/code&gt; object, which has keys for each category, each of which is an
object with a &lt;code&gt;score&lt;/code&gt; property that is a value between 0 and 1.&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ...
  &lt;span class=&quot;token property&quot;&gt;&quot;categories&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;performance&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ...
      &lt;span class=&quot;token property&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.99&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;accessibility&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ...
      &lt;span class=&quot;token property&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;best-practices&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ...
      &lt;span class=&quot;token property&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;seo&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ...
      &lt;span class=&quot;token property&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ...
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To generate the metrics report add the JavaScript file &lt;code&gt;lighthouse-metrics.js&lt;/code&gt;
to the project. In this case JavaScript is used for simplicity since this is run
a Node.js container already. This module uses the &lt;code&gt;FORM_FACTOR&lt;/code&gt; environment
variable defined in the job to determine the results file, opens the file and
parses the JSON results, creates an array of formatted metrics with scores from
0 to 100, then joins them together, one on each line, and finally saves the
resulting metrics file.
&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining&quot;&gt;Optional chaining&lt;/a&gt;
is used when retrieving the metrics so that any individual missing metric do not
raise an error, it simply reports the value as &lt;code&gt;NaN&lt;/code&gt; (since it returns
&lt;code&gt;undefined&lt;/code&gt; multiplied by a number).&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; formFactor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FORM_FACTOR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scoreScalingFactor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; reportFileName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;lighthouse-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;formFactor&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.report.json&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; metricsFileName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;lighthouse-metrics.txt&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; categories &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;performance&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;accessibility&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;best-practices&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;seo&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; results &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;reportFileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; metrics &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; categories
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;category&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;lighthouse{category=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;category&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;,form_factor=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;formFactor&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;} &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;
        results&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;categories&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;category&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;score &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; scoreScalingFactor
      &lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;metricsFileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; metrics&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this script available the CI job needs to be updated to run this in the
jobs. It is executed in
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#after_script&quot;&gt;&lt;code&gt;after_script&lt;/code&gt;&lt;/a&gt; versus
&lt;code&gt;script&lt;/code&gt; since a failure in &lt;code&gt;after_script&lt;/code&gt; does not fail the job, and the job
should not fail only for a metrics-related failure. This is run with the &lt;code&gt;node&lt;/code&gt;
command since project files pulled by the GitLab runner are not given executable
permission. The CI job &lt;code&gt;artifacts&lt;/code&gt; also needs to be updated with a
&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/#artifactsreports&quot;&gt;&lt;code&gt;reports&lt;/code&gt;&lt;/a&gt; property for
GitLab to import the report. Note the metrics report does not need to added to
&lt;code&gt;artifacts:paths&lt;/code&gt; since &lt;code&gt;reports&lt;/code&gt; are ingested directly by GitLab, not saved
with other artifacts.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;after_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node ./lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;metrics.js
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;metrics.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With those updates, the first merge request to create the metrics shows results
like the screenshot below, identifying the 8 new metrics (4 categories * 2 form
factors) and giving the values.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/lighthouse-metrics-new.webp&quot; width=&quot;1196&quot; height=&quot;284&quot; alt=&quot;Metrics in merge request when first added&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;p&gt;In future merge requests, any Lighthouse score changes are identified as changed
values as shown in the screenshot below.&lt;/p&gt;
&lt;img src=&quot;https://aarongoldenthal.com/img/lighthouse-metrics-changed.webp&quot; width=&quot;1196&quot; height=&quot;284&quot; alt=&quot;Metrics in merge request when changed&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
&lt;h2 id=&quot;final-ci-job&quot; tabindex=&quot;-1&quot;&gt;Final CI job &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/running-lighthouse-in-gitlab-ci-with-metrics-report/#final-ci-job&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Putting all of these pieces together, the final two jobs are shown below.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_mobile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; registry.gitlab.com/gitlab&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ci&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;utils/lighthouse&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
  &lt;span class=&quot;token key atrule&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; test
  &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; job_that_builds_site
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;FORM_FACTOR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;mobile&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;before_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; serve ./site_folder/ &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt; /dev/null 2&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;&amp;amp;1&lt;/span&gt; &amp;amp; wait&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;on http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;3000/
  &lt;span class=&quot;token key atrule&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      lighthouse http://localhost:3000/ --chrome-flags=&quot;--headless&quot;
      --only-categories=&quot;accessibility,best-practices,performance,seo&quot;
      --output=&quot;json,html&quot; --output-path=&quot;lighthouse-$FORM_FACTOR&quot;
      $LIGHTHOUSE_ARGUMENTS&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;after_script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; node ./lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;metrics.js
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always
    &lt;span class=&quot;token key atrule&quot;&gt;expose_as&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Lighthouse Report&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mobile.report.html
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;mobile.report.json
    &lt;span class=&quot;token key atrule&quot;&gt;reports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;metrics.txt

&lt;span class=&quot;token key atrule&quot;&gt;lighthouse_desktop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse_mobile
  &lt;span class=&quot;token key atrule&quot;&gt;variables&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;LIGHTHOUSE_ARGUMENTS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;--preset=desktop&#39;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;FORM_FACTOR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;desktop&#39;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;artifacts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;desktop.report.html
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; lighthouse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;desktop.report.json&lt;/code&gt;&lt;/pre&gt;
</content>
    </entry>
    
    <entry>
    <title>The Blog Is Back In Town</title>

    <link href="https://aarongoldenthal.com/posts/the-blog-is-back-in-town/" />
    <updated>2023-01-10T00:00:00Z</updated>
    <id>https://aarongoldenthal.com/posts/the-blog-is-back-in-town/</id>
    <content type="html">&lt;p&gt;It&#39;s been well over a decade since I kept a blog here. That&#39;s a lifetime or two
in software development, but I&#39;ve missed writing and decided it&#39;s time to bring
it back. So, this is the kick-off to incentivize me to write more. And it never
hurts to have another new project to work on, and something with more web
front-end work, that is developing a site with
&lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt;, hosted on GitLab pages, optimizing the use of
GitLab&#39;s tooling, and extensive use of automation with GitLab CI.&lt;/p&gt;
&lt;p&gt;This blog will focus on the software world I live in today and my open source
work, which includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous integration, continuous delivery, continuous deployment, and
automation&lt;/li&gt;
&lt;li&gt;Leveraging GitLab and GitLab CI to optimize software development&lt;/li&gt;
&lt;li&gt;Applications written in Node.js, sometimes Bash, and usually run in containers&lt;/li&gt;
&lt;li&gt;Development of automation and automated test tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of my open source projects are hosted in GitLab in the group
&lt;a href=&quot;https://gitlab.com/gitlab-ci-utils&quot;&gt;GitLab CI Utils&lt;/a&gt; - a mix of GitLab CI jobs,
containers, and Node.js tools/libraries. This blog&#39;s site lives at my
&lt;a href=&quot;https://gitlab.com/aarongoldenthal&quot;&gt;GitLab.com profile&lt;/a&gt;, but there&#39;s not a lot
else there (at least not public). Most of my projects are maintained in a
separate group to facilitate participation in
&lt;a href=&quot;https://about.gitlab.com/solutions/open-source/join/&quot;&gt;GitLab&#39;s Open Source Program&lt;/a&gt;,
which provides free GitLab Ultimate licenses, and the additional capabilities
that brings, to improve open source development.&lt;/p&gt;
&lt;p&gt;I do also have a &lt;a href=&quot;https://github.com/aarongoldenthal&quot;&gt;GitHub profile&lt;/a&gt;, but this
is primarily forks of various open source projects that I contribute to. Many
are one-off contributions, but I do make frequent contribution to
&lt;a href=&quot;https://github.com/pa11y/pa11y&quot;&gt;Pa11y&lt;/a&gt; and
&lt;a href=&quot;https://github.com/pa11y/pa11y-ci&quot;&gt;Pa11y CI&lt;/a&gt; (automated web accessibility
testing tools).&lt;/p&gt;
&lt;h2 id=&quot;blog-history&quot; tabindex=&quot;-1&quot;&gt;Blog history &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-blog-is-back-in-town/#blog-history&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My last blog was written 13 - 15 years ago. At that point it was focused on web
development with ASP.NET. So, for a little entertainment here are some stats on
the state of web development at that time to show how far things have come since
then.&lt;/p&gt;
&lt;h3 id=&quot;browsers&quot; tabindex=&quot;-1&quot;&gt;Browsers &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-blog-is-back-in-town/#browsers&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Internet Explorer still had 50% of the browser market share, and even though
Internet Explorer 9 was the latest release, most web applications were still
written to support Internet Explorer 6/7/8 (that is, before the DOM was
standardized).&lt;/li&gt;
&lt;li&gt;Chrome v5 was the latest release, and it was still using WebKit (yes, that
WebKit).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;web-applications&quot; tabindex=&quot;-1&quot;&gt;Web applications &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-blog-is-back-in-town/#web-applications&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ASP.NET v4.0, using Web Forms, was just released. ASP.NET MVC was a release
candidate and .Net Core was still 6 years away. Enterprises not using ASP.NET
were probably using ColdFusion or Classic ASP.&lt;/li&gt;
&lt;li&gt;There was no such thing as front-end, back-end, or full-stack web developers.
We were just web developers, and we were all full-stack.&lt;/li&gt;
&lt;li&gt;The terms multi-page app (MPA), server-side rendering (SSR), and file system
based routing didn&#39;t exist because all web apps were server-side rendered
multi-page apps using file system based routing (maybe with a little AJAX
sprinkled in).&lt;/li&gt;
&lt;li&gt;Many developers still expected HTML to evolve to some form of XHTML strict.
The WHATWG had formed and created an initial HTML 5 draft spec, but it
wouldn&#39;t be a candidate recommendation for 2 years, and no browser would fully
support it for 3 years.&lt;/li&gt;
&lt;li&gt;REST was just starting to get real adoption as a successor to SOAP for web
services.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;front-end&quot; tabindex=&quot;-1&quot;&gt;Front end &lt;a class=&quot;header-link&quot; href=&quot;https://aarongoldenthal.com/posts/the-blog-is-back-in-town/#front-end&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;It took some courage (and probably a lot of wasted time) to develop
cross-platform or cross-browser web applications without jQuery.&lt;/li&gt;
&lt;li&gt;Many designs (especially some older enterprise apps) still conformed to the
&amp;quot;web safe color palette.&amp;quot;&lt;/li&gt;
&lt;li&gt;Responsive web design was just starting to be used, and Twitter had just
released the Bootstrap framework to make it practical.&lt;/li&gt;
&lt;li&gt;AngularJS came out late that year (that is v1, before v2 was re-named as
Angular). The first release of Typescript was still 2 years away. The first
release of React was 3 years away.&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
</feed>
