<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Cicd on Backend Engineering Strategy Tools</title><link>https://backend-engineering-strategy-tools.github.io/site/tags/cicd/</link><description>Recent content in Cicd on Backend Engineering Strategy Tools</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 18 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://backend-engineering-strategy-tools.github.io/site/tags/cicd/index.xml" rel="self" type="application/rss+xml"/><item><title>Image Tooling</title><link>https://backend-engineering-strategy-tools.github.io/site/projects/image-tooling/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/projects/image-tooling/</guid><description>&lt;p&gt;Versioned, multi-arch Docker images for Kubernetes workflows — built with &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger&lt;/a&gt;, published to Docker Hub, triggered by a version tag.&lt;/p&gt;
&lt;p&gt;The motivation is in &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/thinking/shared-tooling-images/" &gt;Shared Tooling Images&lt;/a&gt;: one image, consistent versions, three contexts — CI, local, colleagues.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="images"&gt;Images
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;GitHub repo&lt;/th&gt;
 &lt;th&gt;Docker Hub&lt;/th&gt;
 &lt;th&gt;Contents&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;a class="link" href="https://github.com/Backend-Engineering-Strategy-Tools/image-tooling" target="_blank" rel="noopener"
 &gt;&lt;code&gt;image-tooling&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/tooling-k8s&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;kubectl, helm, kustomize, argocd CLI, k9s, jq, yq&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-tooling&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/tooling-k8s-aws&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;tooling-k8s&lt;/code&gt; + AWS CLI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-tooling&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/tooling-k8s-openstack&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;tooling-k8s&lt;/code&gt; + OpenStack CLI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;a class="link" href="https://github.com/Backend-Engineering-Strategy-Tools/image-buildx" target="_blank" rel="noopener"
 &gt;&lt;code&gt;image-buildx&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/buildx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;CI builder — Docker buildx, AWS CLI, Dagger CLI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;a class="link" href="https://github.com/Backend-Engineering-Strategy-Tools/image-pandoc" target="_blank" rel="noopener"
 &gt;&lt;code&gt;image-pandoc&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/pandoc&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;PDF generation — pandoc + TeX Live&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All images publish as multi-arch manifests: &lt;code&gt;linux/amd64&lt;/code&gt; + &lt;code&gt;linux/arm64&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="quick-start"&gt;Quick start
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Interactive shell with kubeconfig mounted:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run -it --rm &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -v ~/.kube:/mnt/kube:ro &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -v &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;pwd&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;:/work &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -w /work &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; docker.io/best-tools/tooling-k8s:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The image entry point symlinks &lt;code&gt;/mnt/kube&lt;/code&gt; → &lt;code&gt;/root/.kube&lt;/code&gt; on startup, so &lt;code&gt;kubectl&lt;/code&gt; picks it up immediately.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shell alias for daily use:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;alias k8s&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;docker run -it --rm \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -v ~/.kube:/mnt/kube:ro \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -v $(pwd):/work -w /work \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker.io/best-tools/tooling-k8s:latest&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;k8s helm lint .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;k8s kubectl get pods -n argocd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;In CI (GitHub Actions):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Lint chart&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker run --rm -v ${{ github.workspace }}:/work -w /work docker.io/best-tools/tooling-k8s:latest helm lint .&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or reference the image directly as the job container — no install step needed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="setup-contributors--maintainers"&gt;Setup (contributors / maintainers)
&lt;/h2&gt;&lt;p&gt;Credentials are set once as &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/github/" &gt;GitHub org-level secrets&lt;/a&gt; and inherited by all &lt;code&gt;image-*&lt;/code&gt; repos automatically.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Secret&lt;/th&gt;
 &lt;th&gt;Where to get it&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;hub.docker.com → Account → Security → Access Tokens (Read, Write, Delete)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;DAGGER_CLOUD_TOKEN&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;cloud.dagger.io → Organisation → Tokens&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Path: github.com/Backend-Engineering-Strategy-Tools → Settings → Secrets and variables → Actions → New organisation secret.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="releasing"&gt;Releasing
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git tag -a v1.0.0 -m &lt;span style="color:#e6db74"&gt;&amp;#34;Release v1.0.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git push origin v1.0.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The GitHub Actions workflow triggers on &lt;code&gt;v*.*.*&lt;/code&gt; tags, calls &lt;code&gt;dagger call publish-multi-arch&lt;/code&gt;, and pushes both &lt;code&gt;best-tools/&amp;lt;image&amp;gt;:v1.0.0&lt;/code&gt; and &lt;code&gt;best-tools/&amp;lt;image&amp;gt;:latest&lt;/code&gt; to Docker Hub. Pipeline trace at &lt;a class="link" href="https://dagger.cloud/" target="_blank" rel="noopener"
 &gt;cloud.dagger.io&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="links"&gt;Links
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/Backend-Engineering-Strategy-Tools" target="_blank" rel="noopener"
 &gt;Backend-Engineering-Strategy-Tools org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://hub.docker.com/u/best-tools" target="_blank" rel="noopener"
 &gt;best-tools on Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger pipelines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>This Site</title><link>https://backend-engineering-strategy-tools.github.io/site/projects/best-site/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/projects/best-site/</guid><description>&lt;p&gt;This started as an effort to clean up and publish existing notes — things accumulated over years of work that were sitting in various raw forms and never properly written up. The act of publishing forces a level of clarity that private notes rarely get: you have to explain the context, fill the gaps, and decide what actually matters.&lt;/p&gt;
&lt;p&gt;It doubles as deliberate practice in documentation. Getting into the habit of writing things up properly — not just as a record of what was done, but as something useful to come back to — is a skill that compounds. The site is both the output and the exercise.&lt;/p&gt;
&lt;p&gt;Practically it also serves as a personal reference: a place to look things up that I have actually done and know the details of, rather than re-deriving them from scratch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Hugo static site using the &lt;a class="link" href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener"
 &gt;Stack theme&lt;/a&gt; v4 via Go modules. Deployed to &lt;a class="link" href="https://pages.github.com/" target="_blank" rel="noopener"
 &gt;GitHub Pages&lt;/a&gt; via GitHub Actions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a class="link" href="https://github.com/Backend-Engineering-Strategy-Tools/site" target="_blank" rel="noopener"
 &gt;Backend-Engineering-Strategy-Tools/site&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-hugo"&gt;Why Hugo
&lt;/h2&gt;&lt;p&gt;Static is the right default for content that does not need dynamic rendering. Hugo is fast — full site builds in milliseconds — and its template model maps well to a Go-centric mindset. The theme ecosystem is good and the Stack theme in particular supports the layout and content model needed here without significant customisation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="build-pipeline"&gt;Build Pipeline
&lt;/h2&gt;&lt;p&gt;The GHA workflow runs on push to &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fetch private assets&lt;/strong&gt; — CV and cover letter PDFs are stored in a private release on a separate repo. The workflow fetches them via GitHub API using a scoped &lt;code&gt;BEST_SITE_PAT&lt;/code&gt; secret and writes them into &lt;code&gt;static/cv/&lt;/code&gt; before the build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hugo build&lt;/strong&gt; — &lt;code&gt;hugo --minify&lt;/code&gt; via the official &lt;code&gt;ghcr.io/gohugoio/hugo&lt;/code&gt; Docker image, producing &lt;code&gt;public/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy&lt;/strong&gt; — &lt;code&gt;actions/deploy-pages&lt;/code&gt; pushes &lt;code&gt;public/&lt;/code&gt; to GitHub Pages.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This keeps CV content separate from site code — the PDF repo can be updated independently without touching the site repo, and a site redeploy picks up the latest version.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="theme-customisation"&gt;Theme Customisation
&lt;/h2&gt;&lt;p&gt;Stack theme v4 is imported via &lt;code&gt;go.mod&lt;/code&gt;. Hugo&amp;rsquo;s lookup order means local files in &lt;code&gt;layouts/&lt;/code&gt; or &lt;code&gt;assets/&lt;/code&gt; override the theme without forking it.&lt;/p&gt;
&lt;p&gt;The main override is &lt;code&gt;layouts/_default/single.html&lt;/code&gt; — the site uses &lt;code&gt;layout: single&lt;/code&gt; on all content pages to get full-width rendering (no sidebar). All pages also set &lt;code&gt;showReadingTime: false&lt;/code&gt; in front matter.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="domain"&gt;Domain
&lt;/h2&gt;&lt;p&gt;Currently served at &lt;code&gt;backend-engineering-strategy-tools.github.io/site/&lt;/code&gt;. A redirect from &lt;code&gt;best.mjnet.info&lt;/code&gt; is set up via an S3 static website bucket — no files in the bucket, just a redirect rule. DNS managed in Route53.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;S3 bucket setup&lt;/strong&gt; (bucket name must match domain):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws s3api create-bucket &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bucket best.mjnet.info &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --region eu-central-1 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --create-bucket-configuration LocationConstraint&lt;span style="color:#f92672"&gt;=&lt;/span&gt;eu-central-1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws s3api put-public-access-block &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bucket best.mjnet.info &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --public-access-block-configuration BlockPublicAcls&lt;span style="color:#f92672"&gt;=&lt;/span&gt;false,IgnorePublicAcls&lt;span style="color:#f92672"&gt;=&lt;/span&gt;false,BlockPublicPolicy&lt;span style="color:#f92672"&gt;=&lt;/span&gt;false,RestrictPublicBuckets&lt;span style="color:#f92672"&gt;=&lt;/span&gt;false
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws s3api put-bucket-website &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --bucket best.mjnet.info &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --website-configuration &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;IndexDocument&amp;#34;:{&amp;#34;Suffix&amp;#34;:&amp;#34;index.html&amp;#34;},&amp;#34;RoutingRules&amp;#34;:[{&amp;#34;Redirect&amp;#34;:{&amp;#34;Protocol&amp;#34;:&amp;#34;https&amp;#34;,&amp;#34;HostName&amp;#34;:&amp;#34;backend-engineering-strategy-tools.github.io&amp;#34;,&amp;#34;ReplaceKeyPrefixWith&amp;#34;:&amp;#34;site/&amp;#34;}}]}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Route53 ALIAS record&lt;/strong&gt; pointing &lt;code&gt;best.mjnet.info&lt;/code&gt; at the S3 website endpoint:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;aws route53 change-resource-record-sets &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --hosted-zone-id &amp;lt;ZONE_ID&amp;gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --change-batch &lt;span style="color:#e6db74"&gt;&amp;#39;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;Changes&amp;#34;: [{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;Action&amp;#34;: &amp;#34;CREATE&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;ResourceRecordSet&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;Name&amp;#34;: &amp;#34;best.mjnet.info&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;Type&amp;#34;: &amp;#34;A&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;AliasTarget&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;HostedZoneId&amp;#34;: &amp;#34;Z21DNDUVLTQW6Q&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;DNSName&amp;#34;: &amp;#34;s3-website.eu-central-1.amazonaws.com&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;EvaluateTargetHealth&amp;#34;: false
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; }&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Z21DNDUVLTQW6Q&lt;/code&gt; is the fixed AWS hosted zone ID for S3 website endpoints in &lt;code&gt;eu-central-1&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="content-model"&gt;Content Model
&lt;/h2&gt;&lt;p&gt;Two sections with distinct purposes:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Section&lt;/th&gt;
 &lt;th&gt;Tone&lt;/th&gt;
 &lt;th&gt;What goes here&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;homelab/&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Narrative, first-person&lt;/td&gt;
 &lt;td&gt;What actually happened — the sequence, dead ends, workarounds&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;public-notes/&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Reference, factual&lt;/td&gt;
 &lt;td&gt;How things work — distilled, structured, without the personal journey&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The separation keeps the narrative and reference writing from muddying each other. A reader looking for a quick reference does not want to read about what went wrong first; a reader following the homelab story wants the context.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;projects/&lt;/code&gt; and &lt;code&gt;thinking/&lt;/code&gt; round out the structure — projects are documented outcomes, thinking is longer-form analysis.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s Next
&lt;/h2&gt;&lt;p&gt;The current setup — Hugo on GitHub Pages — is the right foundation for a content-heavy site. A few things are planned or in progress:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom domain&lt;/strong&gt; — &lt;code&gt;best.mjnet.info&lt;/code&gt; redirect via S3 + Route53 (see Domain section above). Proper custom domain with HTTPS as a follow-up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better build pipeline&lt;/strong&gt; — replacing the Makefile with a &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger&lt;/a&gt; Go module for local/CI parity and pinned Hugo versions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More content&lt;/strong&gt; — the backlog of notes and projects is long. The site grows as things get cleaned up and published.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Longer term, the intent is to grow this into a more multi-faceted site — login-protected sections, dynamic content, interactive tooling. Hugo handles the static part well; the approach is to add separate paths alongside it for authentication and dynamic content rather than replacing the static foundation. The static site stays fast and low-cost, dynamic parts layer in where they are actually needed.&lt;/p&gt;</description></item><item><title>Conftest</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/policy-as-code/conftest/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/policy-as-code/conftest/</guid><description>&lt;p&gt;Conftest is a CLI tool that runs OPA policies against structured config files — Kubernetes manifests, Terraform plans, Helm output, Dockerfiles, GitHub Actions workflows, anything that can be parsed. It is the CI/CD enforcement layer on top of Rego.&lt;/p&gt;
&lt;p&gt;Write a policy once in Rego, run &lt;code&gt;conftest test&lt;/code&gt; in your pipeline, fail the build if violations are found.&lt;/p&gt;
&lt;h2 id="install"&gt;Install
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;brew install conftest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="basic-usage"&gt;Basic usage
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Test a Kubernetes manifest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;conftest test deployment.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Test all YAML in a directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;conftest test k8s/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Test a Terraform plan (JSON output)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;terraform show -json tfplan &amp;gt; tfplan.json
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;conftest test tfplan.json --parser json
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Test Helm-rendered output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm template my-app ./chart | conftest test -
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By default Conftest looks for policies in &lt;code&gt;./policy/&lt;/code&gt;. Change with &lt;code&gt;--policy&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="policy-structure"&gt;Policy structure
&lt;/h2&gt;&lt;p&gt;Policies are Rego files in the &lt;code&gt;policy/&lt;/code&gt; directory. Conftest checks for &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;, and &lt;code&gt;violation&lt;/code&gt; rules.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rego" data-lang="rego"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# policy/k8s.rego&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;package&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# deny blocks the pipeline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;deny &lt;span style="color:#66d9ef"&gt;contains&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;msg&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;kind&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Deployment&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;spec&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;template&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;spec&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;securityContext&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;runAsNonRoot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;msg&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sprintf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Deployment %v must set runAsNonRoot&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;metadata&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# warn prints a warning but does not fail&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;warn &lt;span style="color:#66d9ef"&gt;contains&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;msg&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;kind&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Deployment&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;not&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;metadata&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;labels&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;app.kubernetes.io/version&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;msg&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;sprintf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Deployment %v is missing version label&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;metadata&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;$ conftest test deployment.yaml
FAIL - deployment.yaml - main - Deployment nginx must set runAsNonRoot
WARN - deployment.yaml - main - Deployment nginx is missing version label

2 tests, 0 passed, 1 warning, 1 failure
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="namespaces"&gt;Namespaces
&lt;/h2&gt;&lt;p&gt;Use &lt;code&gt;--namespace&lt;/code&gt; to scope which Rego package Conftest evaluates. Useful when you have per-resource-type policies in separate packages.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;conftest test deployment.yaml --namespace kubernetes.deployments
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="multiple-parsers"&gt;Multiple parsers
&lt;/h2&gt;&lt;p&gt;Conftest supports many input formats. Common ones:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Flag&lt;/th&gt;
 &lt;th&gt;Parses&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--parser yaml&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;YAML (default for &lt;code&gt;.yaml&lt;/code&gt;/&lt;code&gt;.yml&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--parser json&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;JSON, Terraform plan&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--parser hcl2&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Terraform HCL&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--parser dockerfile&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Dockerfiles&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--parser toml&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;TOML&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="sharing-policies"&gt;Sharing policies
&lt;/h2&gt;&lt;p&gt;Policies can be distributed as OCI artifacts and pulled with &lt;code&gt;conftest pull&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;conftest pull ghcr.io/myorg/policies:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;conftest test deployment.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;a class="link" href="https://www.conftest.dev/sharing/" target="_blank" rel="noopener"
 &gt;Conftest policy hub&lt;/a&gt; documents the format. Useful for sharing a common policy library across repos without copy-pasting Rego.&lt;/p&gt;
&lt;h2 id="in-ci"&gt;In CI
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# GitHub Actions example&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Policy check&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; conftest test k8s/ --policy policy/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Exits non-zero on &lt;code&gt;deny&lt;/code&gt; violations. &lt;code&gt;warn&lt;/code&gt; violations print but do not fail the build — useful for phased enforcement (warn first, deny later).&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.conftest.dev/" target="_blank" rel="noopener"
 &gt;Conftest documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/open-policy-agent/conftest" target="_blank" rel="noopener"
 &gt;Conftest GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/open-policy-agent/conftest/tree/master/examples" target="_blank" rel="noopener"
 &gt;Example policies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>These Are Not the Pipelines You Are Looking For</title><link>https://backend-engineering-strategy-tools.github.io/site/thinking/one-command-any-pipeline/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/thinking/one-command-any-pipeline/</guid><description>&lt;p&gt;There are a lot of &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/platforms/" &gt;CI/CD platforms&lt;/a&gt;. GitHub Actions, Tekton, Jenkins, Jenkins X, Harness, Bitbucket Pipelines, GitLab CI, CircleCI, Argo Workflows. The choice between them is a perennial argument — and mostly the wrong one.&lt;/p&gt;
&lt;p&gt;The mistake is putting your build logic inside the pipeline. Once you do that, the pipeline owns your build. Reproducing a failure locally means setting up the CI environment. Switching platforms means rewriting all the steps. Testing the pipeline means pushing a commit and waiting. The platform becomes load-bearing.&lt;/p&gt;
&lt;p&gt;The fix is straightforward: keep all logic in &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/make/" &gt;Make&lt;/a&gt; or &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger&lt;/a&gt;. The pipeline calls one command. &lt;code&gt;make build&lt;/code&gt;. &lt;code&gt;make test&lt;/code&gt;. &lt;code&gt;dagger call publish&lt;/code&gt;. That&amp;rsquo;s it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The pipeline shrinks to thin orchestration: trigger on a git event, check out the code, call the command, report the result. It does not know what the build does. It does not care. You can swap GitHub Actions for Tekton or Tekton for Argo Workflows and the only thing that changes is the YAML wrapper around the same command.&lt;/p&gt;
&lt;p&gt;More importantly: you can run the same command on your laptop. No CI environment to replicate, no &amp;ldquo;it works locally but fails in CI&amp;rdquo; gap to debug. The build is the build, wherever it runs.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This matters more the longer a project lives. Build tools consolidate and diverge. Platforms get acquired, deprecated, or repriced. Jenkins pipelines from five years ago are maintained by people who weren&amp;rsquo;t there when they were written and can&amp;rsquo;t run them outside of Jenkins. The projects that survive these transitions cleanly are the ones where the build logic was never in the pipeline to begin with — it was in a Makefile or a build tool that could run anywhere.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/build-systems/" &gt;Gradle&lt;/a&gt;, Maven, Make — these predate CI/CD platforms by decades and will outlast the current generation of them. Put your logic there. The pipeline is a trigger and a reporter. Keep it that way.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The pattern in practice:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; docker build -t myimage:&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;VERSION&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;test&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; go test ./...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;publish&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; docker push myimage:&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;VERSION&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# GitHub Actions — the entire pipeline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;make build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;make test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;make publish&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Tekton — same command, different wrapper&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;build-test-publish&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;golang:1.22&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;script&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; make build
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; make test
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; make publish&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Same logic. One command. Any pipeline.&lt;/p&gt;</description></item><item><title>Make — Task Runner Pattern</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/make/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/make/</guid><description>&lt;p&gt;Make predates most of the tooling in this notes collection by decades. Originally built to manage C compilation — track which source files changed, recompile only what&amp;rsquo;s needed. The dependency graph and incremental execution model are genuinely elegant.&lt;/p&gt;
&lt;p&gt;In modern use, Make is rarely used for what it was designed for. It is used as a &lt;strong&gt;task runner&lt;/strong&gt;: a consistent interface that wraps whatever the project actually needs to do.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;.PHONY&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; build test lint deploy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;build&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	docker build -t myapp .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;test&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	docker run --rm myapp pytest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;lint&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	docker run --rm myapp ruff check .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;deploy&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	helm upgrade --install myapp ./chart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;make build&lt;/code&gt;, &lt;code&gt;make test&lt;/code&gt;, &lt;code&gt;make lint&lt;/code&gt; — same interface regardless of what&amp;rsquo;s underneath. New team member, CI pipeline, colleague&amp;rsquo;s machine: one place to look.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-it-still-works"&gt;Why it still works
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Installed everywhere. No setup, no version to pin.&lt;/li&gt;
&lt;li&gt;Self-documenting entry point — &lt;code&gt;cat Makefile&lt;/code&gt; tells you how to work with the project.&lt;/li&gt;
&lt;li&gt;Trivially wraps Docker, shell scripts, language-specific tools, cloud CLIs.&lt;/li&gt;
&lt;li&gt;Low overhead — it is just shell execution with a thin layer on top.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fact that it is old is not a weakness. It is stable, understood, and available.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-phony-problem"&gt;The &lt;code&gt;.PHONY&lt;/code&gt; problem
&lt;/h2&gt;&lt;p&gt;Make&amp;rsquo;s native model assumes targets are files. &lt;code&gt;make build&lt;/code&gt; checks if a file named &lt;code&gt;build&lt;/code&gt; exists and skips the recipe if it does. &lt;code&gt;.PHONY&lt;/code&gt; declares that a target is not a file, forcing it to always run.&lt;/p&gt;
&lt;p&gt;Forgetting &lt;code&gt;.PHONY&lt;/code&gt; is a classic Make footgun. Always declare task targets phony.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;.PHONY&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; build test lint clean
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="modern-alternatives"&gt;Modern alternatives
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Just&lt;/strong&gt; (&lt;code&gt;Justfile&lt;/code&gt;) — purpose-built task runner, not a build system. Cleaner syntax than Make, no file-dependency model to work around, better error messages. Cross-platform. Requires installation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Task&lt;/strong&gt; (&lt;code&gt;Taskfile.yml&lt;/code&gt;) — YAML-based, similar goals to Just. More readable for people uncomfortable with Make syntax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why still use Make then&lt;/strong&gt;: zero install cost, ubiquity in CI environments, and for many projects the &lt;code&gt;.PHONY&lt;/code&gt; quirk is the only real friction. If the team already knows Make and the project isn&amp;rsquo;t complex, switching to Just adds a dependency without solving a real problem.&lt;/p&gt;
&lt;p&gt;If starting fresh on a project where the team is comfortable with the tooling choice, Just is the cleaner option.&lt;/p&gt;
&lt;p&gt;It is there, it works. That is not faint praise — ubiquity and stability are real value. Every project gets a Makefile. New person joins, CI pipeline runs, colleague needs to build something: &lt;code&gt;make&lt;/code&gt; is the answer and it requires no explanation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="pattern-shared-interface-over-varied-internals"&gt;Pattern: shared interface over varied internals
&lt;/h2&gt;&lt;p&gt;The real value is not Make specifically — it is the pattern of having one consistent entry point regardless of what&amp;rsquo;s underneath. Whether that is Make, Just, or a &lt;code&gt;scripts/&lt;/code&gt; directory with a &lt;code&gt;run&lt;/code&gt; script, the goal is the same: anyone (person or pipeline) can run the project without knowing its internals.&lt;/p&gt;
&lt;p&gt;See also &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/thinking/shared-tooling-images/" &gt;Shared Tooling Images&lt;/a&gt; — the same principle applied to the tools themselves.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="where-this-pattern-applies"&gt;Where this pattern applies
&lt;/h2&gt;&lt;p&gt;The signal that logic is leaking into the wrong place: when a tool&amp;rsquo;s config format starts looking like a scripting language. That is the point to extract it into a script and wrap in Make.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build systems&lt;/strong&gt;
Gradle compiles, tests, and packages. Deployment logic, Docker builds, release steps → scripts. &lt;code&gt;make build&lt;/code&gt; calls Gradle. &lt;code&gt;make docker&lt;/code&gt; calls a script. &lt;code&gt;make deploy&lt;/code&gt; calls Helm. See &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/build-systems/" &gt;Build Systems&lt;/a&gt; for the full pattern.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Terraform&lt;/strong&gt;: &lt;code&gt;terraform apply&lt;/code&gt; does the infra. Environment selection, var file injection, state backend config, plan review → scripts. &lt;code&gt;make plan&lt;/code&gt;, &lt;code&gt;make apply&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helm&lt;/strong&gt;: Helm packages and deploys. Cluster selection, secret fetching, values overrides → scripts. &lt;code&gt;make deploy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ansible&lt;/strong&gt;: Ansible provisions. Inventory management, vault decryption, pre-flight checks → scripts. &lt;code&gt;make provision&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;
Test runners run tests. Environment setup, test data seeding, coverage reporting → scripts. &lt;code&gt;make test&lt;/code&gt; is the entry point regardless of whether it is pytest, jest, or go test underneath.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Package managers&lt;/strong&gt;
A common npm/yarn trap: stuffing 20 scripts into &lt;code&gt;package.json&lt;/code&gt; until it becomes a second build system. Keep &lt;code&gt;package.json&lt;/code&gt; for dependency management. Orchestration belongs in Make.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static site / Hugo&lt;/strong&gt;
Hugo builds the site. PDF fetching, Docker wrapping, deploy steps → Makefile. The pattern this site already uses.&lt;/p&gt;</description></item><item><title>SLSA — Supply-chain Levels for Software Artifacts</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/slsa/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/slsa/</guid><description>&lt;p&gt;SLSA (pronounced &amp;ldquo;salsa&amp;rdquo;) is a framework for securing the software supply chain. Developed by Google, now under the OpenSSF. The core question it answers: how do you know the artifact you are deploying is actually what was built from the source you think it was?&lt;/p&gt;
&lt;p&gt;The answer is &lt;strong&gt;provenance&lt;/strong&gt;: cryptographically signed metadata that records what built an artifact, from what source, when, and under what conditions. SLSA defines levels of increasing rigour around how that provenance is generated and verified.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-it-matters"&gt;Why it matters
&lt;/h2&gt;&lt;p&gt;The software supply chain is an attack surface. SolarWinds, XZ Utils, Log4Shell — different attack vectors but the same underlying problem: something got into the build or the dependency that should not have been there, and nobody noticed until it was too late.&lt;/p&gt;
&lt;p&gt;SLSA does not prevent all supply chain attacks. It makes certain classes of attack verifiably harder and gives consumers of an artifact a way to check that it was built the way it claims to have been.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-levels"&gt;The levels
&lt;/h2&gt;&lt;p&gt;SLSA defines three build levels (reorganised from the original four in SLSA v1.0):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L1 — Provenance exists&lt;/strong&gt;
The build produces signed provenance documenting what produced the artifact. Any build system, any format. Protects against accidental tampering and gives a starting point for verification.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L2 — Hosted build, signed provenance&lt;/strong&gt;
Build runs on a hosted CI platform (GitHub Actions, Cloud Build, etc.). Provenance is generated and signed by the build platform, not the developer&amp;rsquo;s machine. Harder to falsify — an attacker needs to compromise the build platform, not just a developer laptop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L3 — Hardened build&lt;/strong&gt;
Ephemeral, isolated build environment. Non-falsifiable provenance — the build platform itself signs the provenance in a way that even a compromised build script cannot forge. Parameterless builds where the inputs are fully declared.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="provenance-in-practice"&gt;Provenance in practice
&lt;/h2&gt;&lt;p&gt;Provenance is a JSON document (SLSA uses the &lt;a class="link" href="https://github.com/in-toto/attestation" target="_blank" rel="noopener"
 &gt;in-toto Attestation Framework&lt;/a&gt;) that records:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The source repository and commit&lt;/li&gt;
&lt;li&gt;The build platform and workflow&lt;/li&gt;
&lt;li&gt;The artifact digest(s)&lt;/li&gt;
&lt;li&gt;The builder identity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is signed using &lt;a class="link" href="https://www.sigstore.dev/" target="_blank" rel="noopener"
 &gt;Sigstore&lt;/a&gt; / cosign, which provides keyless signing backed by OIDC identity. No key management required for the common case.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="tooling"&gt;Tooling
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;SLSA GitHub Generator&lt;/strong&gt;: reusable GitHub Actions workflows that generate SLSA L3 provenance for common artifact types (Go binaries, container images, Maven/Gradle packages). The easiest path to L3 on GitHub Actions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cosign&lt;/strong&gt;: signs and verifies container images and provenance attestations. Part of the Sigstore project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;slsa-verifier&lt;/strong&gt;: CLI tool to verify SLSA provenance against an artifact. Used by consumers to check that an artifact meets a required SLSA level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependency Track / Grype / Trivy&lt;/strong&gt;: SBOM and vulnerability scanning tools that complement SLSA (SLSA is about build integrity, SBOM is about knowing what is in the artifact — related but distinct).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="slsa-and-container-images"&gt;SLSA and container images
&lt;/h2&gt;&lt;p&gt;Container images are a natural fit. The image digest is the artifact, provenance attests to the build that produced it, cosign attaches the attestation to the registry.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# verify an image has SLSA provenance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;slsa-verifier verify-image &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ghcr.io/myorg/myimage@sha256:abc123 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --source-uri github.com/myorg/myrepo &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --source-tag v1.0.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/thinking/shared-tooling-images/" &gt;tooling images&lt;/a&gt; pattern — images published to a registry that teams depend on — SLSA provenance is worth adding. It gives consumers a verifiable chain from source to image.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="practical-adoption"&gt;Practical adoption
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;L1&lt;/strong&gt; is low friction. Generate provenance as part of the build, attach it to the release. Most CI platforms make this straightforward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L2&lt;/strong&gt; requires a hosted build platform (most teams already use one) and using the platform&amp;rsquo;s signing rather than rolling your own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;L3&lt;/strong&gt; requires workflow changes — ephemeral environments, parameterless builds, using the SLSA GitHub Generator or equivalent. More investment but achievable for a standard GitHub Actions project.&lt;/p&gt;
&lt;p&gt;Start at L1. Provenance exists, the chain is documented, the habit is established. L2 and L3 follow naturally as the pipeline matures — you are not making a big architectural decision, you are incrementally tightening what you already have.&lt;/p&gt;
&lt;p&gt;In practice: never seen L1, L2, or L3 required explicitly by a client or platform. It is still &amp;ldquo;good idea in theory&amp;rdquo; territory for most teams. That said, the underlying practices — signed builds, traceable artifacts, reproducible pipelines — show up as requirements all the time, just not always under the SLSA name.&lt;/p&gt;
&lt;p&gt;Which points to what SLSA actually is: a name and a framework for practices that are already good ideas. Automate the build, sign the output, record the provenance. Quality follows from automation — it frees up time to iterate, makes review easier, and ensures consistency without manual discipline. SLSA formalises that and gives you a vocabulary to talk about it with others. The levels are a ladder, not a checklist to complete before you can ship.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="related"&gt;Related
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/thinking/shared-tooling-images/" &gt;Shared Tooling Images&lt;/a&gt;: SLSA provenance applies directly to published tooling images&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/github/" &gt;GitHub Actions&lt;/a&gt;: where SLSA GitHub Generator workflows run&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Dagger</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/</guid><description>&lt;p&gt;Your build script works on your laptop. It breaks in CI because a tool version differs. It breaks for a colleague because they&amp;rsquo;re on a different OS. You&amp;rsquo;re writing &lt;code&gt;apt-get install&lt;/code&gt; steps in YAML and maintaining a separate script for local builds.&lt;/p&gt;
&lt;p&gt;Dagger solves this the same way Docker solved &amp;ldquo;works on my machine&amp;rdquo; for applications: run everything in containers. Every pipeline step executes inside a defined container image — identically on your laptop, in GitHub Actions, or inside a Kubernetes pod. The Dagger Engine is a containerised daemon your pipeline calls into. Where it runs is an operational detail; what it does is defined once in code.&lt;/p&gt;
&lt;p&gt;The other thing Dagger gives you that YAML-based CI systems don&amp;rsquo;t: your pipeline is real code. Write it in Go (or TypeScript or Python), build a library of reusable functions, share it across projects, and test it the same way you&amp;rsquo;d test any other code.&lt;/p&gt;
&lt;h2 id="module"&gt;Module
&lt;/h2&gt;&lt;p&gt;A Dagger module is a Go package that exposes pipeline functions. Initialise one in your repo:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dagger init --sdk go --name my-pipeline
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dagger develop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;dagger init&lt;/code&gt; creates the scaffolding. &lt;code&gt;dagger develop&lt;/code&gt; generates the Go bindings and drops you into a module ready to edit. A minimal function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;package&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;context&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;dagger/my-pipeline/internal/dagger&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyPipeline&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;MyPipeline&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;Build&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;context&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Context&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Directory&lt;/span&gt;) &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Container&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dag&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Container&lt;/span&gt;().
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;From&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;golang:1.22&amp;#34;&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;WithDirectory&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/src&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;WithWorkdir&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/src&amp;#34;&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;WithExec&lt;/span&gt;([]&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;go&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;build&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;./...&amp;#34;&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Call it from anywhere — local terminal, CI step, Argo workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dagger call build --src .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="library-pattern"&gt;Library pattern
&lt;/h2&gt;&lt;p&gt;Because a module is just Go code, you build it like any other Go library: shared types, helper functions, unit tests. The pattern that works well in practice is a single internal module that codifies your organisation&amp;rsquo;s conventions — base images, registry credentials, standard lint and test steps — and each project imports it.&lt;/p&gt;
&lt;p&gt;A fix to the shared module propagates everywhere. You&amp;rsquo;re not updating 40 YAML files.&lt;/p&gt;
&lt;p&gt;Testing pipeline logic with &lt;code&gt;go test&lt;/code&gt; works normally. A test that calls &lt;code&gt;dagger call&lt;/code&gt; exercises the real container execution, not a mock. This is the thing YAML pipelines can&amp;rsquo;t give you: a fast feedback loop on the pipeline logic itself before it hits CI.&lt;/p&gt;
&lt;h2 id="multi-arch-builds"&gt;Multi-arch builds
&lt;/h2&gt;&lt;p&gt;Dagger has first-class support for multi-architecture container builds. Pass the platform as an argument:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;m&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;MyPipeline&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;BuildMultiArch&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;ctx&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;context&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Context&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Directory&lt;/span&gt;) []&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Container&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;platforms&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; []&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Platform&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;linux/amd64&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;linux/arm64&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;containers&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; make([]&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Container&lt;/span&gt;, len(&lt;span style="color:#a6e22e"&gt;platforms&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;i&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;platform&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;platforms&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;containers&lt;/span&gt;[&lt;span style="color:#a6e22e"&gt;i&lt;/span&gt;] = &lt;span style="color:#a6e22e"&gt;dag&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Container&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;dagger&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;ContainerOpts&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;Platform&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;platform&lt;/span&gt;}).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;From&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;golang:1.22&amp;#34;&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;WithDirectory&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/src&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;WithWorkdir&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/src&amp;#34;&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;WithExec&lt;/span&gt;([]&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;go&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;build&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;./...&amp;#34;&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;containers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Same function, multiple targets, no extra tooling.&lt;/p&gt;
&lt;h2 id="running-in-kubernetes-with-argo-workflows"&gt;Running in Kubernetes with Argo Workflows
&lt;/h2&gt;&lt;p&gt;Dagger Engine runs as a container. In Kubernetes, you run it as a sidecar or dedicated pod and point &lt;code&gt;dagger call&lt;/code&gt; at it via the &lt;code&gt;_EXPERIMENTAL_DAGGER_RUNNER_HOST&lt;/code&gt; environment variable. Each Argo Workflows step calls &lt;code&gt;dagger call &amp;lt;function&amp;gt;&lt;/code&gt; — Argo handles DAG orchestration, retries, and observability; Dagger handles the build execution inside containers.&lt;/p&gt;
&lt;p&gt;The split is clean: Argo owns workflow-level concerns, Dagger owns build reproducibility.&lt;/p&gt;
&lt;h2 id="key-commands"&gt;Key commands
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Command&lt;/th&gt;
 &lt;th&gt;What it does&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;dagger init --sdk go&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Initialise a new module with the Go SDK&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;dagger develop&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Generate bindings, enter the development loop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;dagger functions&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;List available functions in the current module&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;dagger call &amp;lt;function&amp;gt; [args]&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Invoke a pipeline function&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;dagger run &amp;lt;command&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Run an arbitrary command with Dagger Engine available&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="dagger-cloud"&gt;Dagger Cloud
&lt;/h2&gt;&lt;p&gt;Dagger Cloud is a hosted observability layer for Dagger pipelines. Free tier available.&lt;/p&gt;
&lt;p&gt;Every pipeline run — local or CI — produces a trace: a visualisation of the container graph with per-step timing, cache hit/miss, and log output. The trace is linked from the terminal output and browsable at cloud.dagger.io.&lt;/p&gt;
&lt;p&gt;Enable it by setting one environment variable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export DAGGER_CLOUD_TOKEN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&amp;lt;token&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dagger call build --src .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# → Run URL: https://app.dagger.cloud/runs/&amp;lt;id&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In GitHub Actions, add &lt;code&gt;DAGGER_CLOUD_TOKEN&lt;/code&gt; as an organisation-level secret (see &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/github/" &gt;GitHub Actions&lt;/a&gt;). The &lt;code&gt;dagger/dagger-action&lt;/code&gt; picks it up automatically — no workflow change needed beyond the env var being present.&lt;/p&gt;
&lt;p&gt;Sign up at cloud.dagger.io using GitHub login. Token is under Organisation → Tokens.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.dagger.io/" target="_blank" rel="noopener"
 &gt;Dagger documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.dagger.io/api/sdk/go" target="_blank" rel="noopener"
 &gt;Go SDK reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://daggerverse.dev/" target="_blank" rel="noopener"
 &gt;Daggerverse — community modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://dagger.cloud/" target="_blank" rel="noopener"
 &gt;Dagger Cloud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Shared Tooling Images — One Image, Three Contexts</title><link>https://backend-engineering-strategy-tools.github.io/site/thinking/shared-tooling-images/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/thinking/shared-tooling-images/</guid><description>&lt;p&gt;The problem with per-context tooling: CI uses one version of a linter, a developer&amp;rsquo;s machine has another, a colleague has a third. Someone upgrades locally, the pipeline fails. Someone pins the pipeline, local runs drift. The maintenance surface is the number of places you install tools multiplied by the number of people on the team.&lt;/p&gt;
&lt;p&gt;The fix is one Docker image that travels across all three contexts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt; — the pipeline pulls the image, runs the same tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local development&lt;/strong&gt; — developers run the image instead of installing tools natively&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Colleagues&lt;/strong&gt; — same image, same versions, no setup docs to go stale&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you need to upgrade a tool, you change one Dockerfile and cut a new tag. Everyone gets it on next pull. No coordination required.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-docker"&gt;Why Docker?
&lt;/h2&gt;&lt;p&gt;There are other ways to solve this — dotfiles, Nix, OS package managers, VMs, cloud-init. All valid in different contexts.&lt;/p&gt;
&lt;p&gt;The reason Docker makes sense here is that we are already working in a Kubernetes ecosystem where containers are the starting point. Most CI systems support Docker and OCI-compatible images natively. So the question becomes: is it worth introducing a different tool management approach alongside containers, or is it less costly to stay consistent?&lt;/p&gt;
&lt;p&gt;Personally, I lean toward staying consistent. Your mileage may vary depending on your stack.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="what-goes-in-the-image"&gt;What Goes in the Image
&lt;/h2&gt;&lt;p&gt;The principle: include anything that needs to be consistent across contexts. Linters, formatters, build tools, CLI utilities.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Not the runtime — that is the application&amp;rsquo;s own image.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;What specifically goes in depends on the target platform. A Kubernetes-only image looks different from one that also includes a cloud provider CLI. Adding tools increases the maintenance burden, so keep each image focused. That is also why there are multiple images — see &lt;a class="link" href="#the-repos" &gt;The Repos&lt;/a&gt; below.&lt;/p&gt;
&lt;p&gt;Typical candidates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;helm&lt;/code&gt;, &lt;code&gt;kustomize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Linters and formatters for the languages in use&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;yq&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt; and similar utilities&lt;/li&gt;
&lt;li&gt;Cloud provider CLI where needed (&lt;code&gt;aws&lt;/code&gt;, &lt;code&gt;openstack&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="versioning"&gt;Versioning
&lt;/h2&gt;&lt;p&gt;Follow SemVer — image tag equals version. Merge to main triggers a release. No need to complicate it beyond trunk-based development.&lt;/p&gt;
&lt;p&gt;Before merging, build and publish an image from the branch so the change can be tested and verified before it lands on mainline. The branch image is short-lived and not promoted to &lt;code&gt;latest&lt;/code&gt; until the merge.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="usage-patterns"&gt;Usage Patterns
&lt;/h2&gt;&lt;p&gt;A shell alias or Makefile target wraps the &lt;code&gt;docker run&lt;/code&gt; so nobody has to remember the full invocation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;alias toolbox&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;docker run -it --rm -v &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;pwd&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:/work -w /work image:latest&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then &lt;code&gt;toolbox helm lint .&lt;/code&gt; or &lt;code&gt;toolbox kubectl apply -f .&lt;/code&gt; works the same locally as it does in CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interactive sessions&lt;/strong&gt; are where this pattern pays off beyond CI. Drop into a persistent terminal with the right context already loaded — kubeconfig mounted in, cloud credentials available, working directory set:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run -it --rm &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -v &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;pwd&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;:/work &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -v ~/.kube:/root/.kube:ro &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -w /work &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image:latest bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You get a shell that looks the same for everyone on the team, regardless of what is installed on their machine.&lt;/p&gt;
&lt;p&gt;For interactive use it is worth including a help script that runs on entry — either via &lt;code&gt;ENTRYPOINT&lt;/code&gt; or by sourcing it from &lt;code&gt;.bashrc&lt;/code&gt; inside the image. It should print what tools are available, their versions, and any common usage examples. Keeps the image self-documenting: a new colleague drops in and immediately knows what they have to work with without reading a README.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# example /usr/local/bin/toolbox-help&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;=== Toolbox ===&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;kubectl &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;kubectl version --client -o json | jq -r &lt;span style="color:#e6db74"&gt;&amp;#39;.clientVersion.gitVersion&amp;#39;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;helm &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;helm version --short&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;aws &lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;aws --version 2&amp;gt;&amp;amp;1&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;Run &amp;#39;toolbox-help&amp;#39; at any time to see this again.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In CI the step just references the image directly — no installation step, no version pinning in the pipeline config beyond the image tag.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-repos"&gt;The Repos
&lt;/h2&gt;&lt;p&gt;Three tooling images, each a superset of the previous, plus two supporting images:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;GitHub repo&lt;/th&gt;
 &lt;th&gt;Docker Hub&lt;/th&gt;
 &lt;th&gt;Contents&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-tooling&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/tooling-k8s&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;kubectl, helm, kustomize, argocd, k9s, jq, yq&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-tooling&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/tooling-k8s-aws&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;tooling-k8s&lt;/code&gt; + AWS CLI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-tooling&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/tooling-k8s-openstack&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;tooling-k8s&lt;/code&gt; + OpenStack CLI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-buildx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/buildx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;CI builder — Docker buildx, AWS CLI, Dagger CLI&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;image-pandoc&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;best-tools/pandoc&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;PDF generation — pandoc + TeX Live&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Repo names use &lt;code&gt;image-&amp;lt;purpose&amp;gt;&lt;/code&gt; in the &lt;a class="link" href="https://github.com/Backend-Engineering-Strategy-Tools" target="_blank" rel="noopener"
 &gt;Backend-Engineering-Strategy-Tools&lt;/a&gt; org. Docker Hub names drop the prefix and just describe the tool. The three tooling flavours share one &lt;code&gt;image-tooling&lt;/code&gt; monorepo so dependency updates and scanning are configured once.&lt;/p&gt;
&lt;p&gt;Pipelines are written in Go using &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger&lt;/a&gt; — the same pipeline runs locally and in CI, publishing multi-arch images (amd64 + arm64) triggered by a version tag.&lt;/p&gt;
&lt;hr&gt;</description></item><item><title>ArgoCD</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/</guid><description>&lt;p&gt;&lt;img alt="ArgoCD" class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="268" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/argo.png" width="268"&gt;&lt;/p&gt;
&lt;p&gt;You deploy with &lt;code&gt;kubectl apply&lt;/code&gt; from your laptop. It works. Then a colleague edits a deployment directly on the cluster to fix something urgent. Now what is running no longer matches what is in Git. That is drift, and it is silent — until something breaks in production and nobody can explain why the live state differs from the last known good config.&lt;/p&gt;
&lt;p&gt;So you use ArgoCD. Git becomes the single source of truth. Every change flows through a pull request, gets reviewed, and syncs to the cluster automatically. If anyone touches a resource directly, ArgoCD detects the divergence and overrides it back. The cluster converges to Git, always.&lt;/p&gt;
&lt;p&gt;This is GitOps: the deployment pipeline is driven by Git state, not by humans running commands.&lt;/p&gt;
&lt;h2 id="ci-vs-cd"&gt;CI vs CD
&lt;/h2&gt;&lt;p&gt;A useful mental separation: CI and CD are different concerns and should be handled by different tools.&lt;/p&gt;
&lt;p&gt;&lt;img alt="CI/CD flow" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="540" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/cicd_flow.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/cicd_flow_hu_6a2bb36163cfd265.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/cicd_flow.png 960w" width="960"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CI&lt;/strong&gt; (Continuous Integration) is about code — build, test, produce an artifact (a container image). A pipeline in GitHub Actions, Tekton, or Jenkins owns this. It ends with an image pushed to a registry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CD&lt;/strong&gt; (Continuous Delivery) is about cluster state — take that artifact and make sure the right version is running in the right environment. ArgoCD owns this. It watches Git, not the CI pipeline.&lt;/p&gt;
&lt;p&gt;Keeping them separate means your deployment logic is not buried inside a CI pipeline that developers need to understand and maintain. ArgoCD runs in the cluster and continuously reconciles state. It is always on.&lt;/p&gt;
&lt;h2 id="applications"&gt;Applications
&lt;/h2&gt;&lt;p&gt;ArgoCD manages &lt;strong&gt;Applications&lt;/strong&gt; — a CRD that maps a Git source to a cluster destination:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Application&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;my-app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;argocd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;project&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;default&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;source&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;repoURL&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/myorg/my-app-config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;targetRevision&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;manifests/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;destination&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;server&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://kubernetes.default.svc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;my-app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;syncPolicy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;automated&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;prune&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;selfHeal&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;prune: true&lt;/code&gt; — resources removed from Git are deleted from the cluster.
&lt;code&gt;selfHeal: true&lt;/code&gt; — any manual change to the cluster is immediately reverted.&lt;/p&gt;
&lt;h2 id="app-of-apps"&gt;App of Apps
&lt;/h2&gt;&lt;p&gt;Managing dozens of Applications individually gets unwieldy. The &lt;strong&gt;App of Apps&lt;/strong&gt; pattern solves this: one root Application whose source is a directory of other Application manifests. ArgoCD applies the root, which creates all the child Applications, which in turn sync their own workloads. One repo, one sync, everything deployed.&lt;/p&gt;
&lt;h2 id="sync-strategies"&gt;Sync strategies
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Strategy&lt;/th&gt;
 &lt;th&gt;Behaviour&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Automated&lt;/td&gt;
 &lt;td&gt;ArgoCD syncs on every Git change automatically&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Manual&lt;/td&gt;
 &lt;td&gt;Changes are detected and shown as OutOfSync — a human triggers the sync&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Automated sync with selfHeal is the purest GitOps posture. Manual sync is useful for production environments where you want a human approval step before changes roll out.&lt;/p&gt;
&lt;h2 id="rollback"&gt;Rollback
&lt;/h2&gt;&lt;p&gt;Because every state the cluster has ever been in corresponds to a Git commit, rollback is a &lt;code&gt;git revert&lt;/code&gt; — or clicking &amp;ldquo;Sync to previous revision&amp;rdquo; in the ArgoCD UI. No special tooling, no runbooks, just Git history.&lt;/p&gt;
&lt;h2 id="repo-structure"&gt;Repo structure
&lt;/h2&gt;&lt;p&gt;A layout that works well in practice separates ArgoCD&amp;rsquo;s own installation from the workloads it manages:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cluster/&amp;lt;cluster&amp;gt;/
 cfg/argo-cd/ # ArgoCD install only — CRDs and Helm values
 app-of-apps/ # Root Application, Projects, app definitions
 overlay/&amp;lt;app&amp;gt;/ # Per-cluster Kustomize patches, secret/config overrides

external/ # Reusable base manifests shared across clusters
internal/ # Internal app base manifests
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The key separations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ArgoCD install is isolated&lt;/strong&gt; in &lt;code&gt;cfg/argo-cd&lt;/code&gt; to avoid recursive install loops and make upgrades predictable. ArgoCD is not managing its own installation yet at this point — that comes later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App-of-Apps lives separately&lt;/strong&gt; from the install. Once ArgoCD is running, applying &lt;code&gt;app-of-apps/&lt;/code&gt; bootstraps the entire cluster in one step.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base vs overlay&lt;/strong&gt; — &lt;code&gt;external/&lt;/code&gt; and &lt;code&gt;internal/&lt;/code&gt; define &lt;em&gt;what an app is&lt;/em&gt;. The cluster overlay defines &lt;em&gt;how it runs in this environment&lt;/em&gt;. Cluster-specific concerns (resource limits, replica counts, secret refs) stay in the cluster directory and never bleed into the base.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bootstrapping-a-cluster"&gt;Bootstrapping a cluster
&lt;/h2&gt;&lt;p&gt;There is a chicken-and-egg problem: ArgoCD manages everything, but something has to install ArgoCD first. The two-step bootstrap solves it cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Install ArgoCD manually (once):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo add argo https://argoproj.github.io/argo-helm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;helm install argocd argo/argo-cd &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -n argo-cd --create-namespace &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -f cluster/staging/cfg/argo-cd/values.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 2 — Apply the App-of-Apps root:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl apply -k cluster/&amp;lt;cluster&amp;gt;/app-of-apps/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From this point ArgoCD reconciles the entire cluster. Every subsequent change goes through Git — you never run &lt;code&gt;helm install&lt;/code&gt; or &lt;code&gt;kubectl apply&lt;/code&gt; for workloads again.&lt;/p&gt;
&lt;h2 id="self-management"&gt;Self-management
&lt;/h2&gt;&lt;p&gt;The final step is making ArgoCD manage its own upgrades. Create an Application that points at &lt;code&gt;cluster/&amp;lt;cluster&amp;gt;/cfg/argo-cd&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Application&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;argocd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;argocd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;project&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;default&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;source&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;repoURL&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/myorg/cluster-config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;targetRevision&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cluster/staging/cfg/argo-cd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;destination&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;server&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://kubernetes.default.svc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;namespace&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;argocd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;syncPolicy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;automated&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;prune&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt; &lt;span style="color:#75715e"&gt;# be cautious pruning ArgoCD&amp;#39;s own resources&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;selfHeal&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now ArgoCD upgrades itself when you update the Helm values in Git. No more manual &lt;code&gt;helm upgrade&lt;/code&gt; — the cluster is fully self-managing. Changes to ArgoCD config go through the same PR review process as everything else.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://argo-cd.readthedocs.io/" target="_blank" rel="noopener"
 &gt;ArgoCD documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://opengitops.dev/" target="_blank" rel="noopener"
 &gt;GitOps principles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/" target="_blank" rel="noopener"
 &gt;ArgoCD best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>CI/CD Platforms</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/platforms/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/platforms/</guid><description>&lt;p&gt;There are many CI/CD platforms and the choice between them matters less than it appears. All of them are thin orchestration wrappers — trigger on a git event, run some steps, report the result. The build logic itself should live in &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/make/" &gt;Make&lt;/a&gt; or &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger&lt;/a&gt;, not inside the pipeline definition. See &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/thinking/one-command-any-pipeline/" &gt;One Command, Any Pipeline&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="cruisecontrol"&gt;CruiseControl
&lt;/h2&gt;&lt;p&gt;The early pioneer, released in 2001. CruiseControl introduced continuous integration as a practice — polling a source repository, building on every change, sending email on failure. Configuration was XML, the dashboard was a web page, and it ran on a server you managed yourself. Most of the concepts in modern CI trace back here. Largely historical today but worth knowing as the origin point.&lt;/p&gt;
&lt;h2 id="hudson"&gt;Hudson
&lt;/h2&gt;&lt;p&gt;Hudson was Sun Microsystems&amp;rsquo; take on CI — a Java application with a plugin ecosystem that made it far more extensible than CruiseControl. It gained wide adoption in enterprise Java shops during the late 2000s. When Oracle acquired Sun, the project forked. The community fork became Jenkins; the Oracle-maintained branch kept the Hudson name and eventually faded. Hudson is effectively dead.&lt;/p&gt;
&lt;h2 id="jenkins"&gt;Jenkins
&lt;/h2&gt;&lt;p&gt;The fork that won. Jenkins took Hudson&amp;rsquo;s plugin architecture and ran with it — today it has over 1800 plugins covering almost every tool in the ecosystem. A Jenkinsfile defines the pipeline as Groovy DSL and lives in the repository alongside the code. Jenkins is the most widely deployed self-hosted CI server and the default answer in many enterprises. The flip side: it is heavyweight, the Groovy DSL has sharp edges, and complex pipelines are difficult to test outside of Jenkins itself.&lt;/p&gt;
&lt;h2 id="jenkins-x"&gt;Jenkins X
&lt;/h2&gt;&lt;p&gt;Jenkins X is a cloud-native reimagining of Jenkins for Kubernetes. It imposes a strongly opinionated GitOps workflow — pull requests promote through environments, preview environments spin up automatically, everything is driven by Git events. Built on Tekton under the hood. If you want opinionated Kubernetes CI/CD without building the conventions yourself, Jenkins X is one answer. If you want more control over the pipeline structure, raw Tekton gives you the primitives without the opinions.&lt;/p&gt;
&lt;h2 id="tekton"&gt;Tekton
&lt;/h2&gt;&lt;p&gt;Kubernetes-native CI/CD where pipelines, tasks, and triggers are all Kubernetes CRDs — defined in YAML and applied to a cluster, running as pods. No separate CI server to maintain, no external SaaS dependency. CI runs in the same cluster as your workloads using the same RBAC, secrets, and storage primitives. The core primitives are &lt;code&gt;Task&lt;/code&gt; (a sequence of container steps), &lt;code&gt;Pipeline&lt;/code&gt; (an ordered set of tasks), &lt;code&gt;PipelineRun&lt;/code&gt; (an execution), and &lt;code&gt;Trigger&lt;/code&gt; (an event listener that creates runs). The pattern that works well: keep Tasks thin and have them call &lt;code&gt;make &amp;lt;target&amp;gt;&lt;/code&gt; — Tekton handles orchestration, Make handles logic.&lt;/p&gt;
&lt;h2 id="github-actions"&gt;GitHub Actions
&lt;/h2&gt;&lt;p&gt;GitHub&amp;rsquo;s built-in CI/CD, available to any repository on GitHub. Zero infrastructure, zero setup — if your code is on GitHub, Actions is already there. Workflows are YAML files in &lt;code&gt;.github/workflows/&lt;/code&gt;, triggered by git events, running on GitHub-managed runners. A large marketplace of pre-built actions covers most common tasks. The zero-friction default for open source projects and small teams. See the &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/github/" &gt;GitHub Actions&lt;/a&gt; note for more detail.&lt;/p&gt;
&lt;h2 id="argo-workflows"&gt;Argo Workflows
&lt;/h2&gt;&lt;p&gt;A Kubernetes-native workflow engine from the Argo project. Where Tekton models CI primitives (Tasks, Pipelines), Argo Workflows is a general-purpose DAG executor — it can run any containerised workload as a directed acyclic graph of steps, with fan-out, fan-in, conditionals, and retry logic. Widely used as the execution layer under other tools (including Dagger on Kubernetes). Pairs well with &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/" &gt;ArgoCD&lt;/a&gt; for a fully Argo-based GitOps stack. See the &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-project/" &gt;Argo&lt;/a&gt; note for coverage of the full Argo ecosystem including Rollouts, Events, and Kargo.&lt;/p&gt;
&lt;h2 id="bitbucket-pipelines"&gt;Bitbucket Pipelines
&lt;/h2&gt;&lt;p&gt;Bitbucket&amp;rsquo;s built-in CI/CD, integrated directly with Atlassian&amp;rsquo;s hosting. If your code is already in Bitbucket, Pipelines is the zero-infrastructure option — the same position GitHub Actions occupies for GitHub users. Workflows are YAML, steps run in Docker containers, and Atlassian handles the runner infrastructure. Tightly integrated with Jira for deployment tracking. The right choice when you&amp;rsquo;re already in the Atlassian ecosystem and don&amp;rsquo;t want to introduce a separate CI tool.&lt;/p&gt;
&lt;h2 id="harness"&gt;Harness
&lt;/h2&gt;&lt;p&gt;A commercial platform with a broader scope than most CI/CD tools — it covers CI, CD, feature flags, cloud cost management, and security testing under one roof. Enterprise-focused, with AI-assisted pipeline generation and strong support for policy and governance across large engineering organisations. Harness is the answer when the organisation needs a managed platform with SLAs, support, and audit trails rather than self-hosted infrastructure. Pricing reflects that positioning.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://tekton.dev/docs/" target="_blank" rel="noopener"
 &gt;Tekton documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.jenkins.io/doc/" target="_blank" rel="noopener"
 &gt;Jenkins documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://jenkins-x.io/docs/" target="_blank" rel="noopener"
 &gt;Jenkins X documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://argoproj.github.io/argo-workflows/" target="_blank" rel="noopener"
 &gt;Argo Workflows documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://support.atlassian.com/bitbucket-cloud/docs/bitbucket-pipelines/" target="_blank" rel="noopener"
 &gt;Bitbucket Pipelines documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://developer.harness.io/" target="_blank" rel="noopener"
 &gt;Harness documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>GitHub Actions</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/github/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/github/</guid><description>&lt;p&gt;For a small project or proof of concept, the cost of building a CI environment often exceeds the cost of the project itself. Spinning up a Tekton cluster, installing Argo Workflows, or maintaining a Jenkins server is real infrastructure — with real setup time, real maintenance overhead, and real dependencies to keep running.&lt;/p&gt;
&lt;p&gt;GitHub Actions is already there. If your code is on GitHub, you have CI. No extra infrastructure, no additional accounts, a marketplace of pre-built integrations for almost everything you need. For open source projects and small teams it is the pragmatic default: free, integrated, and good enough for the standard build → test → publish pipeline.&lt;/p&gt;
&lt;h2 id="workflow-anatomy"&gt;Workflow anatomy
&lt;/h2&gt;&lt;p&gt;Workflows live in &lt;code&gt;.github/workflows/&lt;/code&gt; as YAML files. A workflow has triggers, jobs, and steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build and publish&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;main]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;pull_request&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;branches&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;main]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;make build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;make test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Push image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;if&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;github.ref == &amp;#39;refs/heads/main&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;${{ secrets.DOCKER_HUB_TOKEN }}&amp;#34; | docker login -u ${{ secrets.DOCKER_HUB_USER }} --password-stdin
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker push myorg/myimage:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;on:&lt;/code&gt; defines what triggers the workflow. &lt;code&gt;runs-on:&lt;/code&gt; selects the runner. &lt;code&gt;uses:&lt;/code&gt; pulls a pre-built action from the marketplace. &lt;code&gt;run:&lt;/code&gt; executes shell commands directly.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;if:&lt;/code&gt; condition on the push step is a common pattern: run tests on every pull request, but only publish on merge to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="secrets"&gt;Secrets
&lt;/h2&gt;&lt;p&gt;GitHub Secrets store credentials without putting them in the repository. Add them under &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt;, then reference them in workflows as &lt;code&gt;${{ secrets.MY_SECRET }}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For pushing to external services — Docker Hub, a package registry, a cloud provider — this is the integration point. The workflow authenticates using the secret, the secret itself never appears in logs or code.&lt;/p&gt;
&lt;h3 id="organisation-secrets"&gt;Organisation secrets
&lt;/h3&gt;&lt;p&gt;Secrets can be set at the organisation level and inherited by all repositories — no per-repo configuration needed. Useful for shared CI credentials reused across many repos.&lt;/p&gt;
&lt;p&gt;Path: github.com/&lt;code&gt;&amp;lt;org&amp;gt;&lt;/code&gt; → Settings → Secrets and variables → Actions → &lt;strong&gt;New organisation secret&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set &lt;strong&gt;Repository access&lt;/strong&gt; to &amp;ldquo;All repositories&amp;rdquo; or a selected subset once you know which repos need it.&lt;/p&gt;
&lt;p&gt;A worked example — shared image publishing credentials:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;DOCKERHUB_TOKEN # hub.docker.com → Account → Security → Access Tokens
DAGGER_CLOUD_TOKEN # cloud.dagger.io → Organisation → Tokens
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Set once at org level; every &lt;code&gt;image-*&lt;/code&gt; repo inherits them. Workflows reference them identically to repo secrets: &lt;code&gt;${{ secrets.DOCKERHUB_TOKEN }}&lt;/code&gt;. See &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/dagger/" &gt;Dagger&lt;/a&gt; for &lt;code&gt;DAGGER_CLOUD_TOKEN&lt;/code&gt; context.&lt;/p&gt;
&lt;h2 id="environments"&gt;Environments
&lt;/h2&gt;&lt;p&gt;Environments add a protection layer on top of secrets. Define named environments (e.g. &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;) and configure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Required reviewers&lt;/strong&gt; — a human must approve before the job runs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment secrets&lt;/strong&gt; — secrets scoped to that environment only, not available to other jobs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait timer&lt;/strong&gt; — a mandatory delay before deployment proceeds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A deployment job targets an environment by name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;deploy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;production&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Deploy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./deploy.sh&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;API_KEY&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.PROD_API_KEY }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The job pauses for approval before running. Useful for open source projects where the pipeline is public but deployment credentials are not.&lt;/p&gt;
&lt;h2 id="marketplace-actions"&gt;Marketplace actions
&lt;/h2&gt;&lt;p&gt;Most common tasks have a pre-built action. A few worth knowing:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;What it does&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;actions/checkout&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Clone the repository&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;actions/setup-go&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Install a specific Go version&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;docker/setup-buildx-action&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Enable multi-arch Docker builds&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;docker/build-push-action&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Build and push a container image&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;helm/kind-action&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Spin up a local Kubernetes cluster for tests&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Using marketplace actions trades control for speed. For a POC or small project that&amp;rsquo;s the right trade. For anything critical, pin the action to a specific commit SHA rather than a tag — tags are mutable.&lt;/p&gt;
&lt;h2 id="self-hosted-runners"&gt;Self-hosted runners
&lt;/h2&gt;&lt;p&gt;GitHub-hosted runners (&lt;code&gt;ubuntu-latest&lt;/code&gt;, &lt;code&gt;windows-latest&lt;/code&gt;) cover most cases. Self-hosted runners make sense when you need access to an internal network, specific hardware (a GPU, specialised build machine), or want to avoid per-minute billing at scale.&lt;/p&gt;
&lt;p&gt;At that point you are closer to running a full CI platform, which is where tools like &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/platforms/" &gt;Tekton&lt;/a&gt; or &lt;a class="link" href="https://argoproj.github.io/argo-workflows/" target="_blank" rel="noopener"
 &gt;Argo Workflows&lt;/a&gt; become relevant — they give you the same kind of pipeline orchestration but running entirely on your own infrastructure.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.github.com/en/actions" target="_blank" rel="noopener"
 &gt;GitHub Actions documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/marketplace?type=actions" target="_blank" rel="noopener"
 &gt;Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions" target="_blank" rel="noopener"
 &gt;Workflow syntax reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>