<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Gitlab-Pages on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/gitlab-pages/</link><description>Recent content in Gitlab-Pages on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Mon, 06 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/gitlab-pages/index.xml" rel="self" type="application/rss+xml"/><item><title>The build gate these sites never had</title><link>https://phpboyscout.uk/the-build-gate-these-sites-never-had/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/the-build-gate-these-sites-never-had/</guid><description>&lt;img src="https://phpboyscout.uk/the-build-gate-these-sites-never-had/cover-the-build-gate-these-sites-never-had.png" alt="Featured image of post The build gate these sites never had" /&gt;&lt;p&gt;I run three Hugo sites now. This blog, my dad&amp;rsquo;s B&amp;amp;B, and the vanlife site my wife and I are slowly putting together. All three build with Hugo and deploy to GitLab Pages, and until recently all three had the same blind spot: the build only ever ran on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Which means that if I pushed something that broke the build, a typo in a shortcode, a cross-link to a page that doesn&amp;rsquo;t exist, a theme bump that didn&amp;rsquo;t take, I found out &lt;em&gt;after&lt;/em&gt; it landed. The change was already merged, sitting on the default branch, and the deploy was the thing that fell over. No gate, no warning, nothing telling me on the merge request that the site wouldn&amp;rsquo;t build. Just a red pipeline on &lt;code&gt;main&lt;/code&gt; and a site that had just stopped updating.&lt;/p&gt;
&lt;h2 id="three-copies-of-the-same-job"&gt;Three copies of the same job
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve &lt;a class="link" href="https://phpboyscout.uk/ci-you-include-not-copy/" &gt;written before&lt;/a&gt; about pulling shared CI into components you &lt;em&gt;include&lt;/em&gt; rather than copy-paste, and this was a textbook case of why. Each of the three sites had its own hand-rolled &lt;code&gt;pages&lt;/code&gt; job: the same pinned &lt;code&gt;hugomods/hugo&lt;/code&gt; image, the same &lt;code&gt;GIT_DEPTH: 0&lt;/code&gt;, the same nightly rebuild, all written out three times and drifting apart a little more with every tweak. I already had a &lt;code&gt;zensical-pages&lt;/code&gt; component for the docs framework I use elsewhere, but nothing covered a plain Hugo build.&lt;/p&gt;
&lt;p&gt;So I wrote one: a &lt;code&gt;hugo-pages&lt;/code&gt; component in my shared &lt;code&gt;cicd&lt;/code&gt; project, and pointed all three sites at it.&lt;/p&gt;
&lt;h2 id="the-gate-they-were-missing"&gt;The gate they were missing
&lt;/h2&gt;&lt;p&gt;The interesting part isn&amp;rsquo;t the deduplication, it&amp;rsquo;s what the component fixes. It splits the work into two jobs, and that split is the entire reason the component exists.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hugo-build&lt;/code&gt; builds the site to &lt;code&gt;public/&lt;/code&gt;, and it carries an explicit rule so it runs in merge-request pipelines too:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$CI_COMMIT_TAG&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;never&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;$[[ inputs.mr_gate ]]&amp;#34; == &amp;#34;true&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;on_success&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That second line is the bit the old jobs never had. Push a broken Hugo build now and it shows up red on the merge request, where you can see it before merging, instead of sailing onto &lt;code&gt;main&lt;/code&gt; and leaving the deploy as the only thing left to fall over. It&amp;rsquo;s &lt;code&gt;interruptible: true&lt;/code&gt;, so pushing a fix cancels the in-flight run rather than queuing behind it. The deploy is a separate job, &lt;code&gt;pages&lt;/code&gt;, which only runs where it should:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;GIT_STRATEGY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;none&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$CI_PIPELINE_SOURCE == &amp;#34;merge_request_event&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;never&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$CI_COMMIT_BRANCH == &amp;#34;$[[ inputs.deploy_branch ]]&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$CI_PIPELINE_SOURCE == &amp;#34;schedule&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It takes the artifact &lt;code&gt;hugo-build&lt;/code&gt; already produced and republishes it, with &lt;code&gt;GIT_STRATEGY: none&lt;/code&gt; because there&amp;rsquo;s nothing to clone. Build everywhere, catch the breakage early; deploy only from &lt;code&gt;main&lt;/code&gt; (and one other place, which is where it gets interesting). The component is pinned at &lt;a class="link" href="https://gitlab.com/phpboyscout/cicd/-/blob/65bd094158de5eb2d6389f607cf02ae282d633a7/templates/hugo-pages.yml" target="_blank" rel="noopener"
 &gt;v0.13.0&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-one-component-that-wants-to-run-on-a-schedule"&gt;The one component that wants to run on a schedule
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the bit I like. Every other component in my &lt;code&gt;cicd&lt;/code&gt; project carries a leading guard that says &lt;em&gt;never run on a schedule&lt;/em&gt;, because a nightly Renovate schedule firing a release job or a deploy is exactly the kind of pointless busywork you don&amp;rsquo;t want.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hugo-pages&lt;/code&gt; does the opposite, on purpose. It deploys on &lt;code&gt;schedule&lt;/code&gt;, and the reason is sitting in plain sight in the component itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-mysql" data-lang="mysql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Schedule note: unlike every other component (which carries a
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# `schedule → never` guard ...), hugo-pages DELIBERATELY deploys on
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# `schedule`. A nightly pipeline schedule is the mechanism by which
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# future-dated, non-draft posts go live once their date arrives (Hugo
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# excludes future-dated content from `production` builds until then).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# The schedule IS the feature.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When I write a post and date it next Tuesday, Hugo&amp;rsquo;s production build simply doesn&amp;rsquo;t include it yet, because its date is in the future. Nothing publishes it on the day except a build that runs &lt;em&gt;on&lt;/em&gt; the day and finds that the date has now arrived. That&amp;rsquo;s the nightly schedule. It&amp;rsquo;s not noise to be guarded against here, it&amp;rsquo;s the publish button. The post you&amp;rsquo;re reading went live exactly this way: written days early, dated forward, and pushed out by a scheduled pipeline the morning its date came up.&lt;/p&gt;
&lt;p&gt;One codebase, two flatly opposite rules about the same trigger, and knowing which site wants which is the actual design.&lt;/p&gt;
&lt;h2 id="a-gate-you-can-switch-off"&gt;A gate you can switch off
&lt;/h2&gt;&lt;p&gt;There was one wrinkle. The new MR build gate is the right default for a docs site with contributors, but for these three blogs it was friction. I run them trunk-based: I work on &lt;code&gt;main&lt;/code&gt;, I audit locally, I push. A mandatory merge-request build for a workflow that doesn&amp;rsquo;t really use merge requests is a job that mostly just sits there.&lt;/p&gt;
&lt;p&gt;So the gate is a boolean you can turn off:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mr_gate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;boolean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Left at its default, &lt;code&gt;hugo-build&lt;/code&gt; runs everywhere and the gate is on, which is the safe posture for anyone adopting the component fresh. Set it to &lt;code&gt;false&lt;/code&gt;, as this blog does, and &lt;code&gt;hugo-build&lt;/code&gt; falls through to running only where it deploys, matching the classic Pages-only-on-&lt;code&gt;main&lt;/code&gt; setup the sites had before, but now without three copies of the YAML to maintain. Safe by default, loose by choice, and the choice is one line.&lt;/p&gt;
&lt;h2 id="where-it-leaves-things"&gt;Where it leaves things
&lt;/h2&gt;&lt;p&gt;A reusable component instead of three drifting copies, a build gate that catches a broken site before it merges, a schedule that publishes tomorrow&amp;rsquo;s post on time rather than firing for nothing, and an opt-out for the trunk-based sites that don&amp;rsquo;t want the gate. None of it is clever. It just means the next time I fat-finger a shortcode, I find out on the merge request and not from my dad asking why the B&amp;amp;B site is showing a stack trace.&lt;/p&gt;</description></item></channel></rss>