🔄 Mise à jour — juin 2026. Le build cross-repo de Tapaz tourne désormais en Gitea Actions (et
prsm-studioest devenu Gitea-primaire) ; le digest est patché dans lek3s-homelabhébergé sur Gitea. La mécanique cross-repo décrite reste valable, seuls la forge et le runner changent. Voir CI self-hosted avec Gitea Actions.
Le point de départ : un repo de source distinct
Les apps précédentes du cluster (blog, silex, n8n…) ont toutes leurs manifests ET leur Dockerfile committés dans le repo k3s-homelab. Très pratique pour une démo, mais pas la norme dès qu’une app a sa vraie vie ailleurs.
tapaz est la première à inverser le modèle :
- Code source dans
YellowBeanie/prsm-studio(un site React/Vite/TypeScript fait à part) - Manifests Kubernetes dans
YellowBeanie/k3s-homelab/tapaz/ - Image sur
ghcr.io/yellowbeanie/tapaz, buildée par la CI du repo source
La question qui en découle : comment garder un GitOps pur (digest pinné, traçabilité git, rollback git revert) sans transformer chaque release en cha-cha-cha manuel “commit source → noter le digest → editer le manifest → push homelab” ?
Cadre rapide
Le repo source produit l’image. Le repo homelab consomme l’image. ArgoCD watche le repo homelab. Le seul fichier qui relie les deux est k3s-homelab/tapaz/deployment.yaml, ligne :
image: ghcr.io/yellowbeanie/tapaz@sha256:<digest>
Toute la question se résume à : qui réécrit cette ligne après chaque build ?
Trois écoles :
:latestmutable +imagePullPolicy: Always— simple, mais on perd l’historique exact “quelle image tournait quand” et le rollback devient flou.- ArgoCD Image Updater — un controller dans le cluster qui scrute GHCR et patch les manifests tout seul. Élégant, mais ajoute un composant supplémentaire à maintenir, à monitorer et à reconfigurer en cas de pépin.
- CI cross-repo — le workflow du repo source clone le repo homelab, patch avec
sed, commit, push. Zéro composant cluster, le pipeline fait tout le boulot.
J’ai choisi l’option 3 : tout vit déjà dans GitHub Actions, donc autant pousser un cran plus loin.
Le workflow étendu
Le workflow build-image.yml côté prsm-studio fait trois choses désormais : build multi-arch (le cluster est 100% Raspberry Pi ARM64, leçon retenue d’épisodes précédents), push sur GHCR, et bump du digest dans le repo homelab.
name: build-image
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push
id: push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/yellowbeanie/tapaz:latest
ghcr.io/yellowbeanie/tapaz:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Bump digest in k3s-homelab
env:
DIGEST: ${{ steps.push.outputs.digest }}
SRC_SHA: ${{ github.sha }}
HOMELAB_REPO_TOKEN: ${{ secrets.HOMELAB_REPO_TOKEN }}
run: |
git clone --depth 1 --branch actual-state \
https://x-access-token:${HOMELAB_REPO_TOKEN}@github.com/YellowBeanie/k3s-homelab.git homelab
cd homelab
sed -i -E "s|(image: ghcr\.io/yellowbeanie/tapaz@)sha256:[a-f0-9]+|\1${DIGEST}|" tapaz/deployment.yaml
if git diff --quiet; then
echo "Digest already up to date — nothing to commit."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add tapaz/deployment.yaml
git commit -m "tapaz: bump image to ${DIGEST} (prsm-studio@${SRC_SHA})"
git push origin actual-state
Trois détails qui ont leur importance :
git diff --quiet: un rebuild qui produit exactement le même digest (cache GHA hit, contenu identique) ne génère aucun commit. La CI reste idempotente.sed -E "s|(image: ghcr\.io/yellowbeanie/tapaz@)sha256:[a-f0-9]+|\1${DIGEST}|": remplacement strictement ancré sur le préfixeimage: ghcr.io/yellowbeanie/tapaz@pour éviter de toucher d’autres champs si le YAML évolue.github-actions[bot]comme auteur : l’historique homelab montre clairement qu’un commit vient de la CI cross-repo, pas d’un humain. Important pour le grep et pour les blame futurs.
Le PAT côté secret
Pour autoriser le runner à pousser sur k3s-homelab, il faut un PAT (Personal Access Token) avec scope d’écriture sur ce repo. Le GITHUB_TOKEN natif du workflow ne suffit pas — il est cantonné au repo qui l’exécute (prsm-studio).
Création (à raffiner plus tard en fine-grained si voulu) :
gh secret set HOMELAB_REPO_TOKEN \
-R YellowBeanie/prsm-studio \
--body "$PAT_VALUE"
Pour durcir : créer un fine-grained PAT avec uniquement Contents: Read and write sur YellowBeanie/k3s-homelab, et rien d’autre. Le scope minimal protège contre un runner compromis qui tenterait d’aller toucher autre chose.
Cycle complet en pratique
1. git push origin main (sur prsm-studio)
2. CI build-image se déclenche
├─ build amd64 + arm64 (~1m15s avec cache, ~3-4 min sans)
├─ push GHCR ghcr.io/yellowbeanie/tapaz:<sha>
└─ bump digest dans k3s-homelab/tapaz/deployment.yaml
└─ git push origin actual-state (sur k3s-homelab)
3. ArgoCD détecte le commit homelab et sync (~30s)
4. Le pod tapaz pull la nouvelle image et redémarre (~10s)
Du git push initial au pod tournant sur le cluster : environ 2-3 minutes, sans une seule commande à taper en plus.
HOMELAB_REPO_TOKEN. ArgoCD ne watche que k3s-homelab —
il ignore totalement le repo source.
The only thread between the two repos is the cross-repo commit that patches the digest,
authorized by the HOMELAB_REPO_TOKEN PAT. ArgoCD only watches k3s-homelab —
it completely ignores the source repo.
Bonus : sceller le ghcr-pull SealedSecret hors-ligne
L’image GHCR est privée (et je préfère qu’elle le reste — pas envie d’exposer même les bundles JS d’un site avant qu’il soit publié). Donc le namespace tapaz doit avoir un imagePullSecret, lui-même committé en tant que SealedSecret.
Sauf qu’au moment où j’ai voulu le créer, je n’avais pas accès à kubectl sur le cluster depuis le poste où je travaillais. Le workflow kubeseal classique passe par kubeseal --fetch-cert qui suppose une connexion au cluster.
La parade — astuce sous-documentée : kubeseal accepte --cert <fichier.pem> et chiffre 100 % offline. La clé publique du controller, étant publique, est sans risque à committer dans le repo. Et elle y est déjà : docs/sealed-secrets/sealed-secrets-pub.pem.
# Générer le Secret docker-registry en YAML "à la main"
USER=YellowBeanie
TOKEN=ghp_xxxxxxxx # PAT avec read:packages
AUTH=$(printf '%s:%s' "$USER" "$TOKEN" | base64)
DOCKERCFG=$(printf '{"auths":{"ghcr.io":{"username":"%s","password":"%s","auth":"%s"}}}' \
"$USER" "$TOKEN" "$AUTH" | base64 -w0)
cat <<EOF > /tmp/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: ghcr-pull
namespace: tapaz
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: ${DOCKERCFG}
EOF
# Sceller offline
kubeseal --cert docs/sealed-secrets/sealed-secrets-pub.pem \
--format yaml \
--scope strict \
< /tmp/secret.yaml \
> tapaz/sealedsecret-ghcr-pull.yaml
rm /tmp/secret.yaml # ne jamais committer le secret en clair
Le SealedSecret qui en sort est strictement identique à celui qu’aurait produit le workflow kubeseal --fetch-cert. Scope strict = ne peut être déchiffré que dans le namespace tapaz, pour le nom ghcr-pull. Aucun risque de relecture ailleurs si le manifest est exfiltré.
Pourquoi ça scale
Le pattern décrit ici n’est pas spécifique à Tapaz. Le jour où une autre app vient s’ajouter avec son repo source à part, on copie le step “Bump digest” en changeant le sed cible et le secret PAT, et c’est plié. Pas de nouveau controller à apprendre, pas de webhook à configurer, pas de polling à régler. Juste du git et du sed dans un runner.
C’est aussi très observable : tous les bumps automatiques apparaissent sous github-actions[bot] dans l’historique homelab. Un git log --author github-actions tapaz/ donne en deux secondes la timeline des releases successives. Et un rollback à une version antérieure n’est rien d’autre qu’un git revert <commit>.
Leçons retenues
- Avant de réfléchir à un controller (Image Updater, Flux Image automation, etc.), se demander si un step de CI suffit. Pour un homelab à 15 apps, la simplicité opérationnelle gagne presque toujours.
- Committer la clé publique sealed-secrets dans
docs/sealed-secrets/sealed-secrets-pub.pemdébloque le sealing offline. C’est une micro-décision qui paye dès qu’on a un poste sans kubectl ou un collègue à embarquer sur le workflow secrets. - Multi-arch n’est pas optionnel sur un cluster Pi. Le réflexe
platforms: linux/amd64,linux/arm64doit faire partie du workflow par défaut, sans quoi leexec format errorest garanti. - Author bot != humain dans l’historique. Donner un email
github-actions[bot]@users.noreply.github.comaux commits CI évite la confusion six mois plus tard quand on cherche qui a touché à quoi.
Tapaz tourne maintenant en autonomie complète : un push sur prsm-studio devient un pod fraîchement déployé sur le cluster en moins de trois minutes. Et le repo k3s-homelab reste la seule source de vérité de l’état désiré, comme l’exige le contrat GitOps.