<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>grop.dev</title><id>https://grop.dev/feeds/tags/cicd.xml</id><subtitle>Tag: cicd</subtitle><updated>2026-10-03T11:42:51Z</updated><link href="https://grop.dev/feeds/tags/cicd.xml" rel="self" /><link href="https://grop.dev" /><entry><title>Argo CD ApplicationSet with templatePatch</title><id>https://grop.dev/posts/argo-cd-applicationset-with-templatepatch.html</id><author><name>Gunnar Grop</name><email>gunnar.grop@protonmail.com</email></author><updated>2026-02-04T12:39:00Z</updated><link href="https://grop.dev/posts/argo-cd-applicationset-with-templatepatch.html" rel="alternate" /><content type="html">&lt;h2&gt;Abstract&lt;/h2&gt;&lt;p&gt;Deploying applications with Argo CD can be done different ways for several different
reasons. For me, it differ quite a bit between the deployments I maintain professionally,
versus the stuff I deploy in my homelab. In my homelab I don't care as much about stability
or maintainability, and I care more about testing interesting things, quickly, and easily.&lt;/p&gt;&lt;p&gt;Therefore I've been trying to utilize Argo CD's &lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/applicationset-specification/&quot;&gt;ApplicationSet&lt;/a&gt; resource to deploy applications
in my homelab as easily and quickly as possible. Preferably I'd only need a single
ApplicationSet to deploy everything. Something i didn't think possible before I realized that
the &lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/applicationset-specification/&quot;&gt;templatePatch&lt;/a&gt; field can be used instead of &lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/applicationset-specification/&quot;&gt;template&lt;/a&gt; for basically everything.&lt;/p&gt;&lt;h2&gt;Writing ApplicationSets&lt;/h2&gt;&lt;p&gt;Normally, you'd write an ApplicationSet something like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: example
spec:
  goTemplate: true
  goTemplateOptions: [&amp;quot;missingkey=error&amp;quot;]
  generators:
    - git:
        repoURL: https://git.example.com/argocd
        revision: HEAD
        files:
          - path: &amp;quot;**/values.yaml&amp;quot;
  template:
    metadata:
      name: '{{ .path.basename }}'
    spec:
      project: default
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{ .path.basename }}'
      source:
        chart: example-cahrt
        repoURL: https://example.github.io/charts/
        targetRevision: 1.0.0
        helm:
          valueFiles:
            - '{{ .path.path }}/values.yaml'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this case the ApplicationSet uses a git generator to look through a specified git
repository and generates an Application resource for every file it finds called &lt;code&gt;values.yaml&lt;/code&gt;.
Furthermore, the name of the generated Applications will be the name of the directory where
&lt;code&gt;values.yaml&lt;/code&gt; lives.&lt;/p&gt;&lt;p&gt;This ApplicationSet will work great if your're planning on running several instances/deployments
of the same Helm chart! An example git directory structure would look something like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;.
├── app1
│   └── values.yaml
├── app2
│   └── values.yaml
└── app3
    └── values.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But that's not quite what I'm after. In my homelab I run many different charts, with
different requirements. Effectively, I have three distinct types of deployments:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Helm charts with a values file.&lt;/li&gt;&lt;li&gt;Helm charts with a values file and additional manifests.&lt;/li&gt;&lt;li&gt;Just Kubernetes manifests.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;So, with the help of templatePatch I came up with this design:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: meta-applicationset-applications
spec:
  goTemplate: true
  goTemplateOptions: [&amp;quot;missingkey=error&amp;quot;]
  generators:
    - git:
        repoURL: https://git.sr.ht/~gunnargrop/argocd-env-mars.internal
        revision: HEAD
        files:
          - path: &amp;quot;apps/**/meta.yaml&amp;quot;
  templatePatch: |
    spec:
      sources:
        {{- if .sources.chart.enabled }}
        - repoURL: &amp;quot;{{ .sources.chart.repoURL }}&amp;quot;
          chart: &amp;quot;{{ .sources.chart.chart }}&amp;quot;
          targetRevision: &amp;quot;{{ .sources.chart.targetRevision }}&amp;quot;
          helm:
            valueFiles:
              - &amp;quot;$values/{{ .path.path }}/values.yaml&amp;quot;
        {{- end }}
        {{- if .sources.values.enabled }}
        - repoURL: https://git.sr.ht/~gunnargrop/argocd-env-mars.internal
          targetRevision: &amp;quot;{{ .sources.values.targetRevision }}&amp;quot;
          ref: values
        {{- end }}
        {{- if .sources.manifests.enabled }}
        - repoURL: https://git.sr.ht/~gunnargrop/argocd-env-mars.internal
          targetRevision: &amp;quot;{{ .sources.manifests.targetRevision }}&amp;quot;
          path: &amp;quot;{{ .path.path }}/manifests&amp;quot;
        {{- end }}
      {{- with .syncPolicy }}
      syncPolicy:
        {{- toYaml . | nindent 6 }}
      {{- end }}
  template:
    metadata:
      name: &amp;quot;{{ .path.basename }}&amp;quot;
    spec:
      project: &amp;quot;{{ .project }}&amp;quot;
      destination:
        server: https://kubernetes.default.svc
        namespace: &amp;quot;{{ .namespace }}&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The biggest difference here is that &lt;code&gt;spec.source&lt;/code&gt; and &lt;code&gt;spec.syncPolicy&lt;/code&gt; is defined entirely
inside of the templatePatch field. What this allows for is much more powerful gotmpl templating,
where we can have if statements and other handy constructs.&lt;/p&gt;&lt;p&gt;It might be a bit dense to read and understand from a glance, so I'll point out how it works
by showing you what the git repository structure looks like.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;.
└── apps
    ├── app1
    │   ├── meta.yaml
    │   └── values.yaml
    ├── app2
    │   ├── manifests
    │   │   └── secret.yaml
    │   ├── meta.yaml
    │   └── values.yaml
    └── app3
        ├── manifests
        │   ├── deployment.yaml
        │   ├── ingress.yaml
        │   └── service.yaml
        └── meta.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As you can see this ApplicationSet takes care of deploying all of the different deployment
types I have in my homelab. Importantly every &amp;quot;app&amp;quot; in the git repo have a special &lt;code&gt;meta.yaml&lt;/code&gt;
file. This file is just used to specify a bunch of variables that Argo CD will read and use
in the Application templating.&lt;/p&gt;&lt;p&gt;Here's one example of a &lt;code&gt;meta.yaml&lt;/code&gt; that just deploys regular manifests:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;project: default
namespace: default

sources:
  chart:
    enabled: false

  values:
    enabled: false

  manifests:
    enabled: true
    targetRevision: HEAD

syncPolicy:
  automated:
    prune: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here is an example of a &lt;code&gt;meta.yaml&lt;/code&gt; that deploys a Helm chart:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;project: default
namespace: default

sources:
  chart:
    enabled: true
    repoURL: registry-1.docker.io/example
    chart: example
    targetRevision: 1.0.0

  values:
    enabled: true
    targetRevision: HEAD

  manifests:
    enabled: false

syncPolicy:
  automated:
    prune: true

  syncOptions:
    - CreateNamespace=true

  managedNamespaceMetadata:
    labels:
      trust: enabled&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Both of these files will be picked up by the ApplicationSet, which will create one
Application per file.&lt;/p&gt;&lt;p&gt;This is pretty much all I want! Now I no longer have to write any
Application/ApplicationSet resources manually, since I can just create a simple
&lt;code&gt;meta.yaml&lt;/code&gt; file in a directory in the specified git repository, and the one ApplicationSet
will deploy all of my applications.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;As you can see, ApplicationSets are even more flexible than at first glance.
Of course, your needs/wants will probably differ from mine. Maybe you want a different
directory structure, or to supply several values files to a Helm deployment?
Argo CD and it's ApplicationSets can probably handle it.&lt;/p&gt;&lt;p&gt;Hopefully this little article has provided you a starting point for how to templatePatch.&lt;/p&gt;&lt;h2&gt;Related Reads&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/&quot;&gt;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Template/&quot;&gt;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Template/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Security/&quot;&gt;https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Security/&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</content></entry><entry><title>Publishing Static Sites on sourcehut pages</title><id>https://grop.dev/posts/publishing-static-sites-on-sourcehut-pages.html</id><author><name>Gunnar Grop</name><email>gunnar.grop@protonmail.com</email></author><updated>2025-10-22T10:16:00Z</updated><link href="https://grop.dev/posts/publishing-static-sites-on-sourcehut-pages.html" rel="alternate" /><content type="html">&lt;h2&gt;Abstract&lt;/h2&gt;&lt;p&gt;I mentioned in a previous post that I use Kubernetes to deploy my blog.
Since my professional work (and my homelab) consists primarily of managing Kubernetes deployments,
this didn't seem like a bad idea. Albeit a bit overkill. But I had all the infrastructure in place,
so I went with it.&lt;/p&gt;&lt;p&gt;I've also mentioned that I've migrated all my personal git repositories over to &lt;a href=&quot;https://sr.ht/&quot;&gt;sourcehut&lt;/a&gt; recently,
but it wasn't until yesterday that my brain registered that sourcehut has another component, &lt;a href=&quot;https://srht.site/&quot;&gt;pages&lt;/a&gt;,
that allow you to publish static websites... This is way easier!&lt;/p&gt;&lt;p&gt;Not only is it super easy to use, and integrates well with other sourcehut components (git/hg, builds),
but every user even gets their own domain, in the form of &lt;code&gt;username.srht.site&lt;/code&gt;. It's also very easy to
use your own &lt;a href=&quot;https://srht.site/custom-domains/&quot;&gt;custom domains&lt;/a&gt;! This can be done at your domain registrar, by pointing you DNS records to the
following IPv4 (A record) and IPv6 (AAAA record) addresses.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;@ IN A    46.23.81.157&lt;/p&gt;&lt;p&gt;@ IN AAAA 2a03:6000:1813:1337::157&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: These IP addresses might be subject to changes. Please always refer to the &lt;a href=&quot;https://srht.site/custom-domains/&quot;&gt;official documentation&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Anyhow, I thought I might do a little write-up on how to use sourcehut pages, and just to tell you that I've
migrated this blog over to it.&lt;/p&gt;&lt;h2&gt;Publishing your Site&lt;/h2&gt;&lt;p&gt;You'll only need a few things for this to work.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;A &lt;a href=&quot;https://meta.sr.ht/&quot;&gt;sourcehut account&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;A sourcehut git repository containing you static site (I'm using &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt;).&lt;/li&gt;&lt;li&gt;A &lt;a href=&quot;https://srht.site/automating-deployments/&quot;&gt;sourcehut build manifest&lt;/a&gt; to publish your site.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can take a log at the source code for this blog, if you want a reference on what such a git repo might
look like: &lt;a href=&quot;https://git.sr.ht/~gunnargrop/grop.dev&quot;&gt;https://git.sr.ht/~gunnargrop/grop.dev&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The name of the git repository should preferably be the name of the site. In my case, the repo is called &amp;quot;grop.dev&amp;quot;.
In your case it might be called &amp;quot;username.srht.site&amp;quot;.&lt;/p&gt;&lt;p&gt;After you have your git repository, we just have to create a build manifest. This is done by
creating a file called &lt;code&gt;.build&lt;/code&gt; in the root of the repository. The file will look something like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;---
image: alpine/latest

oauth: pages.sr.ht/PAGES:RW

packages:
  - hut
  - hugo

environment:
  site: username.srht.site

tasks:
  - package: |
      cd $site
      hugo
      tar -C public -cvz . &amp;gt; ../site.tar.gz

  - upload: |
      hut pages publish -d $site site.tar.gz&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that's it! After you've created this build manifest, whenever you push changes a commit to your repository,
this build manifest will trigger and publish your Hugo site to your sourcehut page. Which you can then visit at
&lt;a href=&quot;https://username.srht.site&quot;&gt;https://username.srht.site&lt;/a&gt;. 🙂&lt;/p&gt;&lt;p&gt;Of note here is that we're using a tool called &lt;a href=&quot;https://sr.ht/~xenrox/hut/&quot;&gt;hut&lt;/a&gt;, a CLI tool to interact with sourcehut. It authenticates to
sourcehut using &lt;a href=&quot;https://man.sr.ht/meta.sr.ht/oauth.md&quot;&gt;OAuth2 token&lt;/a&gt;, which the build system has a handy keyword for. The &lt;code&gt;oauth&lt;/code&gt; keyword takes
care of generating a token for us, that the CLI tool will use in our build.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# In this case, we generate a token that grants us read/write
# permissions to the sourcehut pages API
oauth: pages.sr.ht/PAGES:RW&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But is this CI good enough? I don't think so, since I don't want changes to get published every time I push something
to my repo. This can be solved by just not triggering the CI when we push something, like so:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;git push --push-option skip-ci&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That's what I do normally, to not unnecessarily waste sourcehut resources, but I also added another part to the build
manifest, so it just publishes my page when I push a new git tag to the repo.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;---
image: alpine/latest

oauth: pages.sr.ht/PAGES:RW

packages:
  - hut
  - hugo

environment:
  site: grop.dev

tasks:
  - setup: |
      # Exit pipeline if NOT triggered by tag
      if [[ &amp;quot;$GIT_REF&amp;quot; != refs/tags/* ]]; then
          complete-build
      fi

  - package: |
      cd $site
      hugo
      tar -C public -cvz . &amp;gt; ../site.tar.gz

  - upload: |
      hut pages publish -d $site site.tar.gz&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;As you can see, it's in fact very easy to publish your own static site to sourcehut pages. I would recommend
giving it a go if you want an easy place to host you blog, or whatever else!&lt;/p&gt;&lt;h2&gt;Related Reads&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://srht.site/&quot;&gt;https://srht.site/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://man.sr.ht/meta.sr.ht/oauth.md&quot;&gt;https://man.sr.ht/meta.sr.ht/oauth.md&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</content></entry><entry><title>Building OCI Images with sourcehut builds</title><id>https://grop.dev/posts/building-oci-images-with-sourcehut-builds.html</id><author><name>Gunnar Grop</name><email>gunnar.grop@protonmail.com</email></author><updated>2025-10-19T22:16:00Z</updated><link href="https://grop.dev/posts/building-oci-images-with-sourcehut-builds.html" rel="alternate" /><content type="html">&lt;h2&gt;Abstract&lt;/h2&gt;&lt;p&gt;More and more I find myself needing custom built &lt;a href=&quot;https://opencontainers.org/&quot;&gt;OCI&lt;/a&gt; images, for various purposes.
Most recently has been this blog, in fact. Since I'm hosting this blog in Kubernetes, one of the most straight forward
ways of keeping it updated and version controlled was to build an OCI image and include the relevant Hugo files in it.&lt;/p&gt;&lt;p&gt;While I'm under no illusions that it's the best way to deploy a Hugo blog, it is very easily maintained and
version controlled. I've also quite recently migrated all of my git repositories over to &lt;a href=&quot;https://sourcehut.org/&quot;&gt;sourcehut&lt;/a&gt;, and this
combined with my, maybe non-traditional, way of deploying this blog has forced me to have a look as sourcehut's CI/CD
system. Simply called &amp;quot;&lt;a href=&quot;https://man.sr.ht/builds.sr.ht/&quot;&gt;builds&lt;/a&gt;&amp;quot;.&lt;/p&gt;&lt;p&gt;I might do a more in-depth review of sourcehut builds at some point, but for now I thought it might be interesting
to share how I've gone about building, publishing and signing OCI images using it.&lt;/p&gt;&lt;p&gt;What I'll be doing in this guide is the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Create a sourehut build manifest.&lt;/li&gt;&lt;li&gt;Building a multi-arch OCI image with &lt;a href=&quot;https://podman.io/&quot;&gt;podman&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;Publishing the OCI image to a registry.&lt;/li&gt;&lt;li&gt;Signing the manifest digest using &lt;a href=&quot;https://github.com/sigstore/cosign&quot;&gt;cosign&lt;/a&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you want to take a look at a real life example, just take a look at the &lt;a href=&quot;https://git.sr.ht/~gunnargrop/grop.dev/tree/2025-10-20&quot;&gt;source code for this blog&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;Prerequisites&lt;/h2&gt;&lt;p&gt;There are a few things you'll need to follow this guide:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;A sourcehut account (of course)&lt;/li&gt;&lt;li&gt;An OCI registry to push images to (Docker Hub, etc)&lt;/li&gt;&lt;li&gt;A &lt;a href=&quot;https://www.sigstore.dev/&quot;&gt;sigstore&lt;/a&gt; key pair (for signing images)&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;sigstore key pair&lt;/h3&gt;&lt;p&gt;If you do not yet have a sigstore keypair, you can simply create one using cosign.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;cosign generate-key-pair&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You'll be prompted for a password, and then the following files will be created in you current directory&lt;/p&gt;&lt;ul&gt;&lt;li&gt;cosign.pub (your public key)&lt;/li&gt;&lt;li&gt;cosign.key (your private key)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To sign anything with your private key, you'll need the password you supplied, of course. And this should go without
saying, but &lt;strong&gt;NEVER&lt;/strong&gt; share your private key or password with anyone.&lt;/p&gt;&lt;h3&gt;Registry authfile&lt;/h3&gt;&lt;p&gt;To effectively, and more safely, handle our OCI registry credentials, we should create an &amp;quot;authfile&amp;quot;.&lt;/p&gt;&lt;p&gt;An authfile looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;{
        &amp;quot;auths&amp;quot;: {
                &amp;quot;registry-1.docker.io&amp;quot;: {
                        &amp;quot;auth&amp;quot;: &amp;quot;dXNlcm5hbWU6cGFzc3dvcmQ=&amp;quot;
                }
        }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where a top-level block &amp;quot;auths&amp;quot; contains a dictionary of registry URIs, and each of those registry elements has an &amp;quot;auth&amp;quot;
field, containing a base64 encoded representation of the string &amp;quot;username:password&amp;quot;.&lt;/p&gt;&lt;p&gt;You can generate the auth string by just piping your credentials to the base64 command (on UNIX systems):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;echo &amp;quot;my-user@example.com:my-secret-password&amp;quot; | base64 --wrap 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Another handy thing of using an authfile is that every tool we're going to use in this tutorial can use it. That includes:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;podman (or docker, if you want)&lt;/li&gt;&lt;li&gt;cosign&lt;/li&gt;&lt;li&gt;skopeo&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For this tutorial I'll juse assume we're dealing with Docker Hub, like in the example above.&lt;/p&gt;&lt;h3&gt;Creating sourcehut Secrets&lt;/h3&gt;&lt;p&gt;Now that we have our sigstore keys and out authfile, we need to create three different secrets in sourcehut, that we'll
use in our pipeline.&lt;/p&gt;&lt;p&gt;If you go to &lt;a href=&quot;https://builds.sr.ht/secrets&quot;&gt;Manage secrets&lt;/a&gt; in sourcehut, you'll have the option to create three types of secrets.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;SSH Key&lt;/li&gt;&lt;li&gt;PGP Key&lt;/li&gt;&lt;li&gt;File&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We'll create three secrets of the &amp;quot;File&amp;quot; type.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;| Secret Name         | Secret Content                   | File Path          | File Mode |
|---------------------|----------------------------------|--------------------|-----------|
| DOCKER_HUB_AUTHFILE | your authfile                    | ~/.auth            | 600       |
| COSIGN_KEY          | your cosign private key          | ~/.cosign.key      | 600       |
| COSIGN_PASSWORD     | your cosign private key password | ~/.cosign.password | 600       |&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;/assets/images/sourcehut/sourcehut_builds_manage_secrets2.png&quot; alt=&quot;image&quot; title=&quot;sourcehut secrets&quot; /&gt;&lt;/p&gt;&lt;p&gt;When we're using these secrets in our pipeline later, they will be mounted as files in our build environment,
at the specified paths and with the specified file permissions.&lt;/p&gt;&lt;h2&gt;Task: Setup&lt;/h2&gt;&lt;p&gt;Now that we have our secrets we just need to create a build manifest for sourcehut builds to use. You can do this either by
creating a files called &amp;quot;.build.yml&amp;quot; in the root in one of your git repositories. Or you can created a directory called
&amp;quot;.builds&amp;quot; and place any &amp;quot;.yml&amp;quot; files in it.&lt;/p&gt;&lt;p&gt;According to the &lt;a href=&quot;https://man.sr.ht/builds.sr.ht/&quot;&gt;documentation&lt;/a&gt;, you can effectively just have up to 4 different build manifests (files) under &amp;quot;.builds&amp;quot;.
If you have more than 4, then a random set of 4 builds will be selected when you're triggering the CI.&lt;/p&gt;&lt;p&gt;For this tutorial though, just one build manifest is plenty. We'll create a file in our git repository that looks like this.
We'll create a file in our git repository called &amp;quot;.builds/publish.yml&amp;quot;&lt;/p&gt;&lt;p&gt;Now we'll start actually writing the build manifest.
First of all, we'll use the &lt;code&gt;alpine/latest&lt;/code&gt; image to run our CI. But you can choose any of the &lt;a href=&quot;https://man.sr.ht/builds.sr.ht/compatibility.md&quot;&gt;available images&lt;/a&gt; here.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;---
image: alpine/latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Secondly we'll install some packages in the build environment. Most of them are obvious, but &lt;code&gt;iptables&lt;/code&gt; it needed for podman
to be able to function correctly, and &lt;code&gt;jq&lt;/code&gt; is just a handy tool to have.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;packages:
  - iptables
  - podman
  - cosign
  - skopeo
  - jq&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, we'll specify which secrets we want to use. This is done by specifying a list of UUIDs, then ones you see
when you go to the &amp;quot;Manage secrets&amp;quot; page.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;secrets:
  - 06965d05-4554-4832-8a60-628e1a219a39  # DOCKER_HUB_AUTHFILE
  - f3e67338-52fb-4d8d-aa4d-7529bbf74cef  # COSIGN_KEY
  - a99495b8-32a3-4b37-9c9f-7e6772dbfba2  # COSIGN_PASSWORD&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we'll specify some environment variables for our build environment. Note that the &lt;code&gt;PROJECT&lt;/code&gt; variable is just the full name
of the git repository, which is what the directory containing the source code will be called.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;environment:
  PROJECT: grop.dev                      # Name of the git repo (will also be the OCI image name)
  OCI_PLATFORM: linux/amd64,linux/arm64  # Platforms to build for
  OCI_REGISTRY: registry-1.docker.io     # Which registry to upload the image to
  OCI_REPOSITORY: gunnargrop             # Which repository to upload the image to&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Lastly, we'll write the tasks list. I usually like to have a setup task to take care of some basic stuff that are needed
for the rest of the tasks.&lt;/p&gt;&lt;p&gt;Here you can see that I've included a task that exits if it's not triggered buy a git tag (as opposed to a commit, etc).
You can do this however you like, but I like running my builds only when I'm pushing a new tag to the repository. The build system
gives us a handy function called &amp;quot;complete-build&amp;quot; that we can call, cleanly exiting the CI.&lt;/p&gt;&lt;p&gt;Note also that when setting environment variables for other tasks to use, we need to write them to the &amp;quot;~/.buildenv&amp;quot; file. This
is a special file that is sourced by all tasks.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tasks:
  - setup: |
      # Exit pipeline if NOT triggered by tag
      if [[ &amp;quot;$GIT_REF&amp;quot; != refs/tags/* ]]; then
          complete-build
      fi

      # Set environment variables
      set +x
      cd $PROJECT
      # Let the VERSION variable be the name of the latest git tag
      echo &amp;quot;VERSION=$(git tag --sort=-v:refname | awk '/^\d/' | head -n 1)&amp;quot; &amp;gt;&amp;gt; ~/.buildenv
      set -x

      # Setup for podman, which requires cgroups to function
      sudo rc-update add cgroups
      sudo rc-service cgroups start&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note here that I've encapsulated the setting of environment variables by &lt;code&gt;set +x&lt;/code&gt; and &lt;code&gt;set -x&lt;/code&gt;. This isn't strictly necessary
here, it's just something I do in every build manifest, in case I want to handle secrets here. And you should &lt;strong&gt;always&lt;/strong&gt; encapsulate
your handling of secrets like this, since this prevents the commands to be written to stdout.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;set +x
# For example
MY_SECRET_VAR=&amp;quot;$(cat ~/.my-secret-password-file)&amp;quot;
set -x&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, here is the full build manifest thus far.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;---
image: alpine/latest

packages:
  - iptables  # Needed for podman
  - podman
  - cosign
  - skopeo
  - jq

secrets:
  - 06965d05-4554-4832-8a60-628e1a219a39  # DOCKER_HUB_AUTHFILE
  - f3e67338-52fb-4d8d-aa4d-7529bbf74cef  # COSIGN_KEY
  - a99495b8-32a3-4b37-9c9f-7e6772dbfba2  # COSIGN_PASSWORD

environment:
  PROJECT: grop.dev
  OCI_PLATFORM: linux/amd64,linux/arm64
  OCI_REGISTRY: registry-1.docker.io
  OCI_REPOSITORY: gunnargrop

tasks:
  - setup: |
      # Exit pipeline if NOT triggered by tag
      if [[ &amp;quot;$GIT_REF&amp;quot; != refs/tags/* ]]; then
      complete-build
      fi

      # Set environment variables
      set +x
      cd $PROJECT
      # Let the VERSION variable be the name of the latest git tag
      echo &amp;quot;VERSION=$(git tag --sort=-v:refname | awk '/^\d/' | head -n 1)&amp;quot; &amp;gt;&amp;gt; ~/.buildenv
      set -x

      # Setup for podman, which requires cgroups to function
      sudo rc-update add cgroups
      sudo rc-service cgroups start&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Task: Build&lt;/h2&gt;&lt;p&gt;Now we need a task to actually build our OCI image. I'd recommend always creating a manifest with podman, instead of just
building images. Creating a manifest simplifies building for multiple architectures, as well as making it possible to push
every image in the manifest with just one command.&lt;/p&gt;&lt;p&gt;Assuming your &lt;code&gt;Dockerfile&lt;/code&gt; is in the root of the repository, we can just cd into the directory and run podman build.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;- build_oci: |
    sudo podman manifest create &amp;quot;$PROJECT:$VERSION&amp;quot;

    cd $PROJECT
    sudo podman build \
    --file &amp;quot;Dockerfile&amp;quot; \
    --platform &amp;quot;$OCI_PLATFORM&amp;quot; \
    --manifest &amp;quot;$PROJECT:$VERSION&amp;quot; \
    .&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Task: Publish&lt;/h2&gt;&lt;p&gt;Now we need to publish the OCI image to our registry. Since we're only running this CI when pushing a git tag, we can assume
the git tag is named after the version we want to set, and because of the setup step we have a handy variable called &lt;code&gt;VERSION&lt;/code&gt;
that we can use.&lt;/p&gt;&lt;p&gt;Note the use of the &lt;code&gt;--authfile&lt;/code&gt; flag here. We're using the authfile mounted at &amp;quot;~/.auth&amp;quot;, from the secret that we created
earlier.&lt;/p&gt;&lt;p&gt;Whenever I push a new version of an image, I also like to push it to the &lt;code&gt;:latest&lt;/code&gt; tag, but you can do this however you like.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;- publish_oci: |
    # Push version
    sudo podman manifest push \
    --authfile ~/.auth \
    &amp;quot;$PROJECT:$VERSION&amp;quot; \
    &amp;quot;$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT:$VERSION&amp;quot;

    # Push latest
    sudo podman manifest push \
    --authfile ~/.auth \
    &amp;quot;$PROJECT:$VERSION&amp;quot; \
    &amp;quot;$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT:latest&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Task: Sign&lt;/h2&gt;&lt;p&gt;Now, after we've pushed the images, we'll sign the index using cosign, using the private key we created earlier.&lt;/p&gt;&lt;p&gt;First we need to copy our authfile to &amp;quot;$HOME/.docker/config.json&amp;quot;, since cosign is hardcoded to read the authfile from this
location. We'll also set the environment variable &lt;code&gt;COSIGN_PASSWORD&lt;/code&gt; to contain the password to our private key, from the secret
that we created earlier. This is a special environment variable that cosign uses, so we don't have to supply cosign commands
with the password. These tasks contains sensitive values, so we'll encapsulate them in an &lt;code&gt;set +x&lt;/code&gt; &lt;code&gt;set -x&lt;/code&gt; block.&lt;/p&gt;&lt;p&gt;Then we'll also use skopeo to get the digest of the OCI index where we pushed our images in the last task. We can use our
regular authfile here since &lt;code&gt;skopeo inspect&lt;/code&gt; takes the &lt;code&gt;--authfile&lt;/code&gt; flag. We'll name the variable &lt;code&gt;DIGEST&lt;/code&gt;. This does &lt;strong&gt;not&lt;/strong&gt;
need to be done for both the &lt;code&gt;:$VERSION&lt;/code&gt; and the &lt;code&gt;:latest&lt;/code&gt; image, since they contain the same images, and have the same digests.&lt;/p&gt;&lt;p&gt;Lastly, we'll sign the images with cosign, using the digest we got from skopeo and the private key we created earlier.
This will push a signature file to the OCI repository, which we can use to verify with our public key later.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;- sign_oci: |
    set +x
    mkdir ~/.docker
    cp ~/.auth ~/.docker/config.json
    export COSIGN_PASSWORD=$(cat ~/cosign.password)
    set -x

    DIGEST=$(skopeo inspect --authfile ~/.auth docker://$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT:$VERSION | jq -r &amp;quot;.Digest&amp;quot;)

    cosign sign --yes \
    --key ~/cosign.key \
    &amp;quot;$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT@$DIGEST&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Complete Build Manifest&lt;/h2&gt;&lt;p&gt;Here is the complete file that we've written.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;---
image: alpine/latest

packages:
  - iptables  # Needed for podman build
  - hugo
  - podman
  - cosign
  - skopeo
  - jq

secrets:
  - 06965d05-4554-4832-8a60-628e1a219a39  # DOCKER_HUB_AUTHFILE
  - f3e67338-52fb-4d8d-aa4d-7529bbf74cef  # COSIGN_KEY
  - a99495b8-32a3-4b37-9c9f-7e6772dbfba2  # COSIGN_PASSWORD

environment:
  PROJECT: grop.dev
  OCI_PLATFORM: linux/amd64,linux/arm64
  OCI_REGISTRY: registry-1.docker.io
  OCI_REPOSITORY: gunnargrop

tasks:
  - setup: |
      # Exit pipeline if NOT triggered by tag
      if [[ &amp;quot;$GIT_REF&amp;quot; != refs/tags/* ]]; then
          complete-build
      fi

      # Set environment variables
      set +x
      cd $PROJECT
      # Let the VERSION variable be the name of the latest git tag
      echo &amp;quot;VERSION=$(git tag --sort=-v:refname | awk '/^\d/' | head -n 1)&amp;quot; &amp;gt;&amp;gt; ~/.buildenv
      set -x

      # Setup for podman, which requires cgroups to function
      sudo rc-update add cgroups
      sudo rc-service cgroups start

  - build_oci: |
      sudo podman manifest create &amp;quot;$PROJECT:$VERSION&amp;quot;

      cd $PROJECT
      sudo podman build \
      --file &amp;quot;Dockerfile&amp;quot; \
      --platform &amp;quot;$OCI_PLATFORM&amp;quot; \
      --manifest &amp;quot;$PROJECT:$VERSION&amp;quot; \
      .

  - publish_oci: |
      # Push version
      sudo podman manifest push \
      --authfile ~/.auth \
      &amp;quot;$PROJECT:$VERSION&amp;quot; \
      &amp;quot;$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT:$VERSION&amp;quot;

      # Push latest
      sudo podman manifest push \
      --authfile ~/.auth \
      &amp;quot;$PROJECT:$VERSION&amp;quot; \
      &amp;quot;$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT:latest&amp;quot;

  - sign_oci: |
      set +x
      mkdir ~/.docker
      cp ~/.auth ~/.docker/config.json
      export COSIGN_PASSWORD=$(cat ~/cosign.password)
      set -x

      DIGEST=$(skopeo inspect --authfile ~/.auth docker://$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT:$VERSION | jq -r &amp;quot;.Digest&amp;quot;)

      cosign sign --yes \
      --key ~/cosign.key \
      &amp;quot;$OCI_REGISTRY/$OCI_REPOSITORY/$PROJECT@$DIGEST&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;We're all done! Now when you push a new tag to your git repository, this CI will run, build your images, publish them,
and sign them.&lt;/p&gt;&lt;p&gt;Once it's completed, you can use cosign with your public key to verify that it's signed by a trusted source.
In my case, it would look like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;cosign verify --key cosign.pub docker.io/gunnargrop/grop.dev:latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where I see this output:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Verification for index.docker.io/gunnargrop/grop.dev:latest --
The following checks were performed on each of these signatures:

-   The cosign claims were validated
-   Existence of the claims in the transparency log was verified offline
-   The signatures were verified against the specified public key

[
  {
    &amp;quot;critical&amp;quot;: {
      &amp;quot;identity&amp;quot;: {
        &amp;quot;docker-reference&amp;quot;: &amp;quot;registry-1.docker.io/gunnargrop/grop.dev&amp;quot;
      },
      &amp;quot;image&amp;quot;: {
        &amp;quot;docker-manifest-digest&amp;quot;: &amp;quot;sha256:91caaa17007095a67be411215c97d989b127c2c9179df6d72258698361797988&amp;quot;
      },
      &amp;quot;type&amp;quot;: &amp;quot;cosign container image signature&amp;quot;
    },
    &amp;quot;optional&amp;quot;: {
      &amp;quot;Bundle&amp;quot;: {
        &amp;quot;SignedEntryTimestamp&amp;quot;: &amp;quot;MEYCIQC0sy5/hKB2+lmJEHOLsyzoDuxG/B9P0sxiOs3wWkozbgIhAMfqqwn/0erKYDc0jRo2x3Cmzd7aNC8oOWOnfUzCaEeb&amp;quot;,
        &amp;quot;Payload&amp;quot;: {
          &amp;quot;body&amp;quot;: &amp;quot;eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJiNWU2ZTQyYjViZWQ2ZmJlMjFlMDZiYzk1ZGFhZGU4MmE4Mjg4MWEyNGVhN2U1NjRmNDk1OTY4MzNkZTUyMzQyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUQrSVFMZzhBTjVJRmh5S09PSEYvcXladWttTHRWZzZTZFR5WlhlaWROcFV3SWdHd2FaS3RHaWJKenVMNi9zQ294c0t0elVPb0h4cC8rYWpFRjAxR0gyV0g4PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGUm1wNVZIVlZaVUYxV1VSVFdXZHJUVWwwT1ZCNk5GWlZVMDFUVFFwTlYydHhRbTV3ZVhCblNXUTNOMUZLUlN0U05FNUNVVGMwWWpReWMzQlVja3hZYWxsV2FGcHJXakl6WVRrdlQyWnpSVzl3UjFGMWVuUjNQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=&amp;quot;,
          &amp;quot;integratedTime&amp;quot;: 1760886624,
          &amp;quot;logIndex&amp;quot;: 622102447,
          &amp;quot;logID&amp;quot;: &amp;quot;c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d&amp;quot;
        }
      }
    }
  }
]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Yay! It works!&lt;/p&gt;&lt;p&gt;If it doesn't work, if there is no signature at all, or if the signature is by an untrusted source, the output will look
like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;Error: no matching signatures: invalid signature when validating ASN.1 encoded signature
error during command execution: no matching signatures: invalid signature when validating ASN.1 encoded signature&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Related Reads&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://man.sr.ht/builds.sr.ht/&quot;&gt;https://man.sr.ht/builds.sr.ht/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://man.sr.ht/builds.sr.ht/compatibility.md&quot;&gt;https://man.sr.ht/builds.sr.ht/compatibility.md&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/sigstore/cosign&quot;&gt;https://github.com/sigstore/cosign&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://git.sr.ht/~gunnargrop/grop.dev/tree/main/item/.builds/publish.yml&quot;&gt;https://git.sr.ht/~gunnargrop/grop.dev/tree/main/item/.builds/publish.yml&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</content></entry></feed>