<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CI-CD on The Pensieve</title><link>https://blog.martin.md/tags/ci-cd/</link><description>Recent content in CI-CD on The Pensieve</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 24 Jul 2026 22:00:00 +0200</lastBuildDate><atom:link href="https://blog.martin.md/tags/ci-cd/index.xml" rel="self" type="application/rss+xml"/><item><title>Migrating from SSH Deploy to git-pages</title><link>https://blog.martin.md/p/git-pages-migration/</link><pubDate>Fri, 24 Jul 2026 22:00:00 +0200</pubDate><guid>https://blog.martin.md/p/git-pages-migration/</guid><description>&lt;h2 id="preamble"&gt;Preamble
&lt;/h2&gt;&lt;p&gt;So I guess this is now a blog about the infrastructure of this blog.
More than a year after the first (and only) post, I&amp;rsquo;m following up the write-up
about how I deployed this blog with one about how I changed that deployment.&lt;/p&gt;
&lt;h2 id="starting-point"&gt;Starting point
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://blog.martin.md/p/hello-friend/#infrastructure" &gt;Last time&lt;/a&gt; I noted that the original themes were meant for deployment on GitHub Pages. But in the spirit of self-hosted&amp;ndash;ness (self-host&amp;ndash;ity?), I decided to go more old-school with an SSH deployment.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem
&lt;/h2&gt;&lt;p&gt;This way of deploying static websites worked fine for decades, but it has a few issues. They all stem from the fact that it requires creating a user with the proper permissions, an SSH key, etc. This is not very scalable (haha, scalable, for one user&amp;hellip;). It also implicitly ties you to the machine the HTTP server is running on. Both of these issues are solvable, but it becomes more and more of a house of cards the more automations you implement.&lt;/p&gt;
&lt;p&gt;Concretely, the blog was deployed by SCP&amp;rsquo;ing Hugo&amp;rsquo;s &lt;code&gt;public/&lt;/code&gt; output to an nginx
container on the server — which in practice meant SSH key management in CI, no
preview deployments for PRs, and manual infrastructure drift.&lt;/p&gt;
&lt;h2 id="the-solution"&gt;The solution
&lt;/h2&gt;&lt;p&gt;It turns out that &lt;a class="link" href="https://treehouse.systems/@whitequark" target="_blank" rel="noopener"
 &gt;Catherine&lt;/a&gt; (of Whitequark fame) saw the same gap, but actually decided to do something about it. So now we have a very cool git-first way of deploying static pages in the form of &lt;a class="link" href="https://codeberg.org/git-pages/git-pages" target="_blank" rel="noopener"
 &gt;git-pages&lt;/a&gt;. All the best features of GitHub Pages, wrapped in an easy HTTP interface, self-hosted and secure by design.&lt;/p&gt;
&lt;h2 id="what-i-did"&gt;What I did
&lt;/h2&gt;&lt;h3 id="created-a-service-repo-for-git-pages"&gt;Created a service repo for git-pages
&lt;/h3&gt;&lt;p&gt;The server-side configuration lives in
&lt;a class="link" href="https://git.martin.md/homelab/dc-git-pages" target="_blank" rel="noopener"
 &gt;homelab/dc-git-pages&lt;/a&gt;, following
the same conventions as my other homelab services.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; — container definition with Traefik labels for routing
&lt;code&gt;pages.martin.md&lt;/code&gt;, &lt;code&gt;*.pages.martin.md&lt;/code&gt;, &lt;code&gt;preview.pages.martin.md&lt;/code&gt;, and
&lt;code&gt;*.preview.pages.martin.md&lt;/code&gt; to git-pages on port 3000&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config.toml&lt;/code&gt; — wildcard domain config, filesystem storage, preview domain
support, and Forgejo authorization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="switched-traefik-to-dns-01-challenge"&gt;Switched Traefik to DNS-01 challenge
&lt;/h3&gt;&lt;p&gt;The Traefik ACME configuration was changed from TLS-ALPN-01 to DNS-01
(Cloudflare). This enables wildcard TLS certificates for &lt;code&gt;*.pages.martin.md&lt;/code&gt;
and &lt;code&gt;*.preview.pages.martin.md&lt;/code&gt;, so any user subdomain gets HTTPS without
manual intervention.&lt;/p&gt;
&lt;h3 id="split-traefik-routers-for-separate-wildcard-certs"&gt;Split Traefik routers for separate wildcard certs
&lt;/h3&gt;&lt;p&gt;Git-pages uses subdomain-based routing (&lt;code&gt;&amp;lt;user&amp;gt;.pages.martin.md/&amp;lt;project&amp;gt;/&lt;/code&gt;),
so Traefik needed &lt;code&gt;HostRegexp&lt;/code&gt; rules for wildcard subdomains.&lt;/p&gt;
&lt;h3 id="rewrote-the-blog-ci-workflow"&gt;Rewrote the blog CI workflow
&lt;/h3&gt;&lt;p&gt;The old workflow used &lt;code&gt;actions/checkout&lt;/code&gt;, &lt;code&gt;peaceiris/actions-hugo&lt;/code&gt;, and
&lt;code&gt;garygrossgarten/github-action-scp&lt;/code&gt; with 4 SSH secrets. The new workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clones the repo directly with &lt;code&gt;git clone&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Installs Hugo from GitHub releases (&lt;code&gt;.deb&lt;/code&gt; package)&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;actions/setup-go@v7&lt;/code&gt; for Go module support&lt;/li&gt;
&lt;li&gt;Builds with &lt;code&gt;--baseURL&lt;/code&gt; set for preview paths&lt;/li&gt;
&lt;li&gt;Creates a tar archive of &lt;code&gt;public/&lt;/code&gt; and PUTs it to git-pages&lt;/li&gt;
&lt;li&gt;Hugo and theme versions are pinned and tracked by Renovate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Production deploys authenticate via DNS TXT challenge
(&lt;code&gt;Authorization: Pages &amp;lt;token&amp;gt;&lt;/code&gt;), previews via Forge-Authorization
(&lt;code&gt;Forge-Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;). No SSH secrets needed.&lt;/p&gt;
&lt;h3 id="set-up-dns-txt-challenge-for-production-domain"&gt;Set up DNS TXT challenge for production domain
&lt;/h3&gt;&lt;p&gt;For &lt;code&gt;blog.martin.md&lt;/code&gt; to be served directly (not via redirect), the CI uses a
DNS TXT challenge token. A &lt;code&gt;_git-pages-challenge.blog.martin.md&lt;/code&gt; TXT record was
created in Cloudflare containing the SHA-256 hash of the string
&lt;code&gt;blog.martin.md &amp;lt;token&amp;gt;&lt;/code&gt; (domain, space, token).
The token is stored as &lt;code&gt;PAGES_DNS_TOKEN&lt;/code&gt; in the blog repo&amp;rsquo;s CI secrets.&lt;/p&gt;
&lt;h3 id="added-commit-status-for-preview-urls"&gt;Added commit status for preview URLs
&lt;/h3&gt;&lt;p&gt;A &lt;code&gt;preview&lt;/code&gt; commit status is posted after each deploy, making the preview URL
clickable directly from the PR view — no need to dig through CI logs.&lt;/p&gt;
&lt;h3 id="added-automatic-preview-cleanup"&gt;Added automatic preview cleanup
&lt;/h3&gt;&lt;p&gt;A separate workflow (&lt;code&gt;cleanup.yml&lt;/code&gt;) fires on &lt;code&gt;pull_request: closed&lt;/code&gt; and sends a
&lt;code&gt;DELETE&lt;/code&gt; to git-pages. Preview sites also auto-expire after 7 days via a
scheduled CI workflow (&lt;code&gt;expire.yml&lt;/code&gt;) in the dc-git-pages repo.&lt;/p&gt;
&lt;h2 id="result"&gt;Result
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;No SSH keys or secrets needed for deployment&lt;/li&gt;
&lt;li&gt;Every PR gets a live preview with a clickable link in the PR view&lt;/li&gt;
&lt;li&gt;Preview sites are cleaned up automatically on merge/close, or expire after 7 days&lt;/li&gt;
&lt;li&gt;Theme and Hugo versions tracked automatically by Renovate&lt;/li&gt;
&lt;li&gt;Production deploys are DNS-validated, preview deploys are CI-verified&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One less SSH key to worry about. Aaaaanyway, I&amp;rsquo;m out.&lt;/p&gt;</description></item></channel></rss>