<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CI/CD on Backend Engineering Strategy Tools</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/</link><description>Recent content in CI/CD on Backend Engineering Strategy Tools</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Wed, 03 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/index.xml" rel="self" type="application/rss+xml"/><item><title>Build Systems — Ant, Maven, Gradle, Bazel</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/build-systems/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/build-systems/</guid><description>&lt;p&gt;The Java ecosystem has cycled through several generations of build tooling. Each generation solved real problems with the previous one and introduced new ones of its own.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="ant"&gt;Ant
&lt;/h2&gt;&lt;p&gt;XML-based, imperative. You describe exactly what to do and in what order — compile these files, copy to this directory, package this jar. No conventions, no opinions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: total control, predictable, easy to understand what any given build does.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: verbose, no built-in dependency management (Ivy was a separate add-on), and every project reinvents the same targets from scratch. Large Ant builds become hard to maintain.&lt;/p&gt;
&lt;p&gt;Still found in older enterprise Java projects. Worth knowing to read and modify, less worth starting from scratch.&lt;/p&gt;
&lt;p&gt;Used it. It was what it was — at least you always knew exactly what the build was doing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="maven"&gt;Maven
&lt;/h2&gt;&lt;p&gt;Convention over configuration. If your project follows the standard layout (&lt;code&gt;src/main/java&lt;/code&gt;, &lt;code&gt;src/test/java&lt;/code&gt;, etc.) most of the build is declared, not scripted. Dependency management built in via the POM and central repository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maven 1&lt;/strong&gt;: the original. Repository model and POM concept introduced. Plugin system was limited and the build lifecycle was rigid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maven 2&lt;/strong&gt;: major redesign. The lifecycle phases (validate → compile → test → package → install → deploy) that most people know. Dependency resolution significantly improved. This is the version that won widespread adoption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maven 3&lt;/strong&gt;: incremental improvements over Maven 2. Better performance, improved parallel builds, polyglot POM support. Most Maven projects today run on 3.x.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: standardised project layout means any Maven project is immediately navigable; dependency management and central repository model that the whole ecosystem built on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: XML is verbose for expressing logic; the lifecycle is powerful but opaque when things go wrong; plugin configuration gets unwieldy; multi-module builds are better than Ant but still awkward at scale.&lt;/p&gt;
&lt;p&gt;Not a fan. All three weaknesses compound each other: the XML is painful to write, the lifecycle hides what is actually executing and where, and parent POM inheritance chains in multi-module projects make it genuinely hard to answer &amp;ldquo;what does this build actually do?&amp;rdquo; You end up cargo-culting POM snippets from Stack Overflow and hoping the lifecycle does what you think it does.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="gradle"&gt;Gradle
&lt;/h2&gt;&lt;p&gt;Groovy (and later Kotlin) DSL instead of XML. Keeps Maven&amp;rsquo;s dependency management and repository model, drops the rigid lifecycle in favour of a task graph. Incremental builds — only re-runs tasks whose inputs changed.&lt;/p&gt;
&lt;p&gt;Became the default for Android builds and has significant adoption in JVM projects that outgrew Maven.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: expressive build scripts; incremental and cached builds make large projects faster; Kotlin DSL gives type safety and IDE support; flexible enough to model non-standard builds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: the flexibility is also the trap — Gradle builds vary wildly and can be hard to read; the Groovy DSL is easy to write badly; build times on cold caches can be slow; debugging task dependencies is non-trivial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gradle vs Maven&lt;/strong&gt;: Maven wins on standardisation and predictability; Gradle wins on performance and flexibility. For a standard Java/Kotlin service with no unusual requirements, Maven is often the lower-maintenance choice. For Android, large monorepos, or complex multi-language builds, Gradle.&lt;/p&gt;
&lt;p&gt;Gradle solves the XML problem but introduces a different one: the flexibility makes it very easy to get wrong. Every team ends up with a slightly different Gradle build, and reading someone else&amp;rsquo;s is often just as opaque as Maven&amp;rsquo;s lifecycle — just for different reasons. The problem of &amp;ldquo;what is this build actually doing&amp;rdquo; doesn&amp;rsquo;t go away, it just changes shape.&lt;/p&gt;
&lt;p&gt;The discipline that keeps Gradle manageable: &lt;strong&gt;don&amp;rsquo;t stuff everything into the Gradle build&lt;/strong&gt;. Gradle should compile, test, and package — that&amp;rsquo;s it. Deployment logic, environment setup, Docker builds, release steps — those belong in scripts or other tools that are good at those things. Then wrap the whole thing in a Makefile that gives a single consistent entry point. &lt;code&gt;make build&lt;/code&gt; calls Gradle. &lt;code&gt;make docker&lt;/code&gt; calls a shell script. &lt;code&gt;make deploy&lt;/code&gt; calls Helm. Gradle stays focused and readable; the Makefile is the seam that holds it together.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="sbt"&gt;SBT
&lt;/h2&gt;&lt;p&gt;The Scala Build Tool. Reactive, incremental compilation at the core. Not just a build tool — the REPL and interactive session are part of the workflow.&lt;/p&gt;
&lt;p&gt;Heavy dependency on Scala itself; the build definition is Scala code. Powerful but a significant learning curve for anyone not already in the Scala ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Notes to follow.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="bazel--bazelisk"&gt;Bazel &amp;amp; Bazelisk
&lt;/h2&gt;&lt;p&gt;Originally Google&amp;rsquo;s internal build system (Blaze), open-sourced as Bazel. Hermetic builds — each action declares its inputs and outputs explicitly, no implicit filesystem access. Correctness and reproducibility guaranteed by construction.&lt;/p&gt;
&lt;p&gt;Language-agnostic: rules exist for Java, Go, Python, C++, and others. Built for monorepos at scale — incremental and distributed builds, remote caching, remote execution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bazelisk&lt;/strong&gt;: version manager for Bazel. Pin the Bazel version in &lt;code&gt;.bazelversion&lt;/code&gt;, run &lt;code&gt;bazelisk&lt;/code&gt; instead of &lt;code&gt;bazel&lt;/code&gt; — it downloads and uses the pinned version automatically. Essential for team consistency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strengths&lt;/strong&gt;: genuinely reproducible builds; scales to very large codebases; remote caching makes CI fast once warm.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weaknesses&lt;/strong&gt;: steep learning curve; rules ecosystem outside Google&amp;rsquo;s own languages is less mature; significant setup cost; overkill for anything smaller than a large monorepo.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Opinions to follow.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="choosing"&gt;Choosing
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;Small project&lt;/th&gt;
 &lt;th&gt;Standard service&lt;/th&gt;
 &lt;th&gt;Large monorepo&lt;/th&gt;
 &lt;th&gt;Polyglot&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Ant&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Possible&lt;/td&gt;
 &lt;td&gt;Legacy only&lt;/td&gt;
 &lt;td&gt;No&lt;/td&gt;
 &lt;td&gt;No&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Maven&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Fine&lt;/td&gt;
 &lt;td&gt;Good default&lt;/td&gt;
 &lt;td&gt;Struggles&lt;/td&gt;
 &lt;td&gt;No&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Gradle&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Fine&lt;/td&gt;
 &lt;td&gt;Good&lt;/td&gt;
 &lt;td&gt;Better&lt;/td&gt;
 &lt;td&gt;Partial&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Bazel&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Overkill&lt;/td&gt;
 &lt;td&gt;Overkill&lt;/td&gt;
 &lt;td&gt;Strong&lt;/td&gt;
 &lt;td&gt;Strong&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both Maven and Gradle have the same core problem: you end up not really knowing what your build is doing. Ant at least was honest about it. If forced to choose, Gradle is the least bad option — the DSL flexibility is a trap but it is a trap you can avoid with discipline, whereas Maven&amp;rsquo;s lifecycle opacity and XML are just the deal regardless. For anything in the JVM ecosystem today the choice is usually made for you by the project or the framework — if you get to choose, Gradle with a simple build file and keep it that way.&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>Argo</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-project/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-project/</guid><description>&lt;p&gt;The Argo project is a suite of Kubernetes-native tools for running and managing workloads and deployments. Each tool solves a distinct problem and they compose well together, but they are independent — you can use any one without the others. All four are CNCF graduated or incubating projects.&lt;/p&gt;
&lt;h2 id="argocd"&gt;ArgoCD
&lt;/h2&gt;&lt;p&gt;GitOps continuous delivery. ArgoCD watches a Git repository and continuously reconciles the cluster state to match it — any drift is detected and corrected automatically. It is the CD half of a modern Kubernetes delivery pipeline: a CI system builds and pushes an image, ArgoCD detects the new tag and rolls it out. See the &lt;a class="link" href="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/argo-cd/" &gt;ArgoCD&lt;/a&gt; note for a full walkthrough including App of Apps, bootstrapping, and self-management.&lt;/p&gt;
&lt;h2 id="argo-workflows"&gt;Argo Workflows
&lt;/h2&gt;&lt;p&gt;A general-purpose workflow execution engine for Kubernetes. Workflows are CRDs that define DAGs or sequential step graphs — each step runs in a container, with outputs passed as artifacts or parameters to downstream steps. Used for CI pipelines, ML training jobs, data processing, and batch workloads. Where Tekton models CI-specific primitives (Tasks, Pipelines), Argo Workflows is lower-level and more flexible: any containerised workload that has dependencies between steps fits the model.&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;Workflow&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;entrypoint&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;build-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;templates&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&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dag&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tasks&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;template&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;run-step&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;arguments&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;parameters&lt;/span&gt;: [{&lt;span style="color:#f92672"&gt;name: cmd, value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;make build&amp;#34;&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;dependencies&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;template&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;run-step&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;arguments&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;parameters&lt;/span&gt;: [{&lt;span style="color:#f92672"&gt;name: cmd, value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;make test&amp;#34;&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;run-step&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;inputs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;parameters&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;cmd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container&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;command&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;sh, -c]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;args&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;{{inputs.parameters.cmd}}&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="argo-rollouts"&gt;Argo Rollouts
&lt;/h2&gt;&lt;p&gt;Progressive delivery for Kubernetes. Where a standard Kubernetes &lt;code&gt;Deployment&lt;/code&gt; does a rolling update (replace pods gradually), Argo Rollouts adds canary and blue-green strategies with analysis gates. A canary rollout shifts a percentage of traffic to the new version, runs automated analysis (checking metrics from Prometheus, Datadog, or similar), and either promotes fully or rolls back based on the result. This makes deployments measurably safer — a bad release fails the analysis gate before it reaches 100% of traffic.&lt;/p&gt;
&lt;h2 id="argo-events"&gt;Argo Events
&lt;/h2&gt;&lt;p&gt;Event-driven automation. Argo Events defines &lt;code&gt;EventSources&lt;/code&gt; (sensors that listen for events — git pushes, S3 uploads, Kafka messages, webhooks, cron schedules) and &lt;code&gt;Sensors&lt;/code&gt; (triggers that respond to those events by creating Argo Workflows, sending notifications, or calling other systems). It is the event bus that ties the rest of the Argo stack together: a git push fires an EventSource, a Sensor detects it and creates a Workflow, the Workflow builds and tests, ArgoCD picks up the new image and rolls it out.&lt;/p&gt;
&lt;h2 id="kargo"&gt;Kargo
&lt;/h2&gt;&lt;p&gt;A newer tool from Akuity (the company behind ArgoCD) that solves multi-stage GitOps promotion. ArgoCD is good at keeping one environment in sync with a Git ref — but promoting a release through dev → staging → production requires updating that ref in each environment and coordinating the sequence. Kargo models this as &lt;code&gt;Stages&lt;/code&gt; with &lt;code&gt;FreightRequests&lt;/code&gt; — a release is a piece of freight that must pass through each stage in order, with optional approval gates between them. It sits above ArgoCD in the stack and handles the promotion logic that ArgoCD deliberately leaves out.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://argoproj.github.io/" target="_blank" rel="noopener"
 &gt;Argo project&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://argoproj.github.io/argo-rollouts/" target="_blank" rel="noopener"
 &gt;Argo Rollouts documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://argoproj.github.io/argo-events/" target="_blank" rel="noopener"
 &gt;Argo Events documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.kargo.io/" target="_blank" rel="noopener"
 &gt;Kargo documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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>Git</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/</guid><description>&lt;p&gt;Git is the distributed version control system behind virtually all modern software development. Every clone is a full copy of the history — fast, local, and cryptographically integrity-checked.&lt;/p&gt;
&lt;h2 id="branches"&gt;Branches
&lt;/h2&gt;&lt;p&gt;Branches in Git are cheap: creating, merging, and deleting them is fast because a branch is just a pointer to a commit, not a copy of the files.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Branch visualization" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="800" 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/git/branches.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/branches_hu_e4c76f5ed2b21162.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/branches.png 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;By convention, one branch is singled out and called &lt;code&gt;main&lt;/code&gt; (historically &lt;code&gt;master&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="workflows"&gt;Workflows
&lt;/h2&gt;&lt;p&gt;How teams structure collaboration varies. Three common models:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benevolent dictator&lt;/strong&gt; — used by the Linux kernel. A hierarchy of trusted lieutenants, with one maintainer having final merge authority.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Benevolent dictator workflow" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="800" 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/git/benevolent-dictator.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/benevolent-dictator_hu_851cdc98802083b8.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/benevolent-dictator.png 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integration manager&lt;/strong&gt; — common in open source. Each contributor has their own public fork; a maintainer pulls from forks into a blessed repository.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Integration manager workflow" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="800" 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/git/integration-manager.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/integration-manager_hu_96ec310b098419c9.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/integration-manager.png 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared repository&lt;/strong&gt; — the most common in companies. Everyone pushes to the same remote (&lt;code&gt;origin&lt;/code&gt;). All instances are equal; &lt;code&gt;origin&lt;/code&gt; is just a convention.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Shared repository workflow" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="800" 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/git/origin.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/origin_hu_a7d4a46c025d1eb0.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/origin.png 1000w" width="1000"&gt;&lt;/p&gt;
&lt;h2 id="data-integrity"&gt;Data integrity
&lt;/h2&gt;&lt;p&gt;Git&amp;rsquo;s data model uses SHA-1 checksums for every file and commit. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every object is verified on checkout&lt;/li&gt;
&lt;li&gt;History cannot be silently altered — changing any commit changes all IDs after it&lt;/li&gt;
&lt;li&gt;You can&amp;rsquo;t push without first pulling (unless you force-push and overwrite)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="merge-vs-rebase"&gt;Merge vs rebase
&lt;/h2&gt;&lt;p&gt;Two ways to integrate branches:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merge&lt;/strong&gt; preserves history exactly as it happened — a merge commit ties the two branches together.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Merge" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="1280" 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/git/merge.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/merge_hu_c092e3335dada9ad.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/merge.png 1600w" width="1600"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rebase&lt;/strong&gt; replays your commits on top of the target branch — produces a linear history but rewrites commit SHAs. Never rebase shared/public branches.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rebase" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="1200" 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/git/rebase.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/rebase_hu_f2a9673967d0c053.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/rebase.png 1600w" width="1600"&gt;&lt;/p&gt;
&lt;h2 id="branch-strategies"&gt;Branch strategies
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Git Flow&lt;/strong&gt; — a structured model with &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;feature/*&lt;/code&gt;, &lt;code&gt;release/*&lt;/code&gt;, and &lt;code&gt;hotfix/*&lt;/code&gt; branches. Good for software with versioned releases.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Git Flow" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="2560" 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/git/git-flow.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/git-flow_hu_42113a80f33ff450.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/git-flow_hu_62391bcd6d3d4fd5.png 1600w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/git-flow_hu_3b1fe4a5068ecdbe.png 2400w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/git-flow.png 3200w" width="3200"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://nvie.com/posts/a-successful-git-branching-model/" target="_blank" rel="noopener"
 &gt;Git Flow original post&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Flow / GitLab Flow&lt;/strong&gt; — simpler: branch from &lt;code&gt;main&lt;/code&gt;, open a pull request, merge when reviewed. Works well for continuously deployed web apps.&lt;/p&gt;
&lt;p&gt;&lt;img alt="GitHub/GitLab Flow" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="1280" 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/git/simple-git-flow.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/simple-git-flow_hu_50e666e72ef5f524.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/git/simple-git-flow.png 1600w" width="1600"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://guides.github.com/introduction/flow/" target="_blank" rel="noopener"
 &gt;GitHub Flow&lt;/a&gt; · &lt;a class="link" href="https://docs.gitlab.com/ee/topics/gitlab_flow.html" target="_blank" rel="noopener"
 &gt;GitLab Flow&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="which-strategy-fits"&gt;Which strategy fits
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Project type&lt;/th&gt;
 &lt;th&gt;Recommendation&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Library / SDK&lt;/td&gt;
 &lt;td&gt;Git Flow — versioned releases, may need to support multiple versions simultaneously&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Web application&lt;/td&gt;
 &lt;td&gt;GitHub Flow — continuously delivered, no need for release branches&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Client / installable app&lt;/td&gt;
 &lt;td&gt;Git Flow — users run specific versions you need to patch and maintain&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="advanced-features"&gt;Advanced Features
&lt;/h2&gt;&lt;h3 id="git-worktree"&gt;Git Worktree
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt; allows you to check out multiple branches of the same repository into separate directories simultaneously. This is particularly useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Working on multiple features or bug fixes in parallel without stashing changes.&lt;/li&gt;
&lt;li&gt;Quickly switching contexts for code reviews or testing different versions of your code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All worktrees share the same Git objects, minimizing disk space usage.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://git-scm.com/docs" target="_blank" rel="noopener"
 &gt;git-scm.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://training.github.com/downloads/github-git-cheat-sheet/" target="_blank" rel="noopener"
 &gt;GitHub Git cheat sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://ndpsoftware.com/git-cheatsheet.html" target="_blank" rel="noopener"
 &gt;Interactive Git cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Gitea / Forgejo</title><link>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/gitea/</link><pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate><guid>https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/gitea/</guid><description>&lt;p&gt;&lt;strong&gt;&lt;a class="link" href="https://gogs.io/" target="_blank" rel="noopener"
 &gt;Gogs&lt;/a&gt;&lt;/strong&gt; came first — a self-hosted Git service, lightweight, single binary, runs anywhere. Think GitHub but on your own infrastructure. Simple to deploy, easy to understand, and that was the point.&lt;/p&gt;
&lt;p&gt;Gitea forked from Gogs in 2016 and has moved considerably faster since — more features, more active development, more community. For any new self-hosted Git deployment, Gitea became the natural choice over Gogs. Unless you look at where Gitea itself is heading.&lt;/p&gt;
&lt;h2 id="forgejo"&gt;Forgejo
&lt;/h2&gt;&lt;p&gt;Gitea has been drifting toward the enterprise and commercial end. The governance has shifted, cloud services have been prioritised, and the community-first ethos that made it attractive has weakened.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="link" href="https://forgejo.org/" target="_blank" rel="noopener"
 &gt;Forgejo&lt;/a&gt;&lt;/strong&gt; is the community response — a fork of Gitea maintained by the Codeberg team, focused on staying open, community-governed, and free. It is a drop-in replacement: same API, same data format, same deployment model. Migrating from Gitea to Forgejo is straightforward.&lt;/p&gt;
&lt;p&gt;For new deployments today, Forgejo is the better choice. Gitea is still solid and maintained, but the trajectory matters.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://codeberg.org/" target="_blank" rel="noopener"
 &gt;Codeberg&lt;/a&gt; — the public hosting platform run by the Forgejo maintainers — is worth knowing about as a GitHub alternative for open source projects.&lt;/p&gt;
&lt;h2 id="features"&gt;Features
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;SSH and HTTPS repository access&lt;/li&gt;
&lt;li&gt;Issues, pull requests, code review, wiki, protected branches&lt;/li&gt;
&lt;li&gt;Webhooks and Git hooks&lt;/li&gt;
&lt;li&gt;Deploy keys and fine-grained access tokens&lt;/li&gt;
&lt;li&gt;LDAP, SMTP, OAuth, OIDC authentication&lt;/li&gt;
&lt;li&gt;PostgreSQL, MySQL, SQLite support&lt;/li&gt;
&lt;li&gt;Built-in container registry&lt;/li&gt;
&lt;li&gt;Gitea Actions / Forgejo Actions — GitHub Actions-compatible CI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="issues"&gt;Issues
&lt;/h2&gt;&lt;p&gt;The issue tracker covers the basics well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Numeric ID, description, comment thread&lt;/li&gt;
&lt;li&gt;Assignable to users, file attachments, labels, milestones&lt;/li&gt;
&lt;li&gt;Open/closed state per repository&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Issue view" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="640" 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/gitea/issue.png" width="800"&gt;&lt;/p&gt;
&lt;p&gt;Default labels — fully customizable:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Labels" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="640" 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/gitea/labels.png" width="800"&gt;&lt;/p&gt;
&lt;p&gt;Organisation-wide issue overview:&lt;/p&gt;
&lt;p&gt;&lt;img alt="All issues" class="gallery-image" data-flex-basis="300px" data-flex-grow="125" height="1760" 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/gitea/all_issues.png" srcset="https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/gitea/all_issues_hu_c8e014b11f98ca1d.png 800w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/gitea/all_issues_hu_47f7964cb5b07fff.png 1600w, https://backend-engineering-strategy-tools.github.io/site/public-notes/cicd/gitea/all_issues.png 2200w" width="2200"&gt;&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://forgejo.org/" target="_blank" rel="noopener"
 &gt;Forgejo&lt;/a&gt; — community fork, recommended for new deployments&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://gitea.io/" target="_blank" rel="noopener"
 &gt;Gitea&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://gogs.io/" target="_blank" rel="noopener"
 &gt;Gogs&lt;/a&gt; — where it all started&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://codeberg.org/" target="_blank" rel="noopener"
 &gt;Codeberg&lt;/a&gt; — public Forgejo hosting&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>