From ab704d19a4c9227ee0edf2214f15feb730c2c0d0 Mon Sep 17 00:00:00 2001 From: valentin2105 Date: Thu, 6 Aug 2026 19:54:15 +1100 Subject: [PATCH 1/5] =?UTF-8?q?[Claude]=20feat:=20red=C3=A9ploiement=20Arg?= =?UTF-8?q?oCD=20via=20ENV=20COMMIT=20+=20hostname=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Job CI "deploy" : réécrit l'ENV COMMIT (SHA court) dans _k8s/overlays//commit-patch.yaml puis commite sur la branche. Le tag d'image reste inchangé ; le changement de pod template suffit à faire resynchroniser ArgoCD et rouler les pods. - Anti-boucle CI : push via GITHUB_TOKEN (ne déclenche pas de workflow), message [skip ci], et paths-ignore sur commit-patch.yaml. - App : ENV COMMIT affichée en petit sous l'IP (fallback "unknown"). - Hostname dev : demoapp-dev.k8s.lab.ops.nc (au lieu de dev.demoapp.*). - Doc README + CLAUDE.md mises à jour. Co-Authored-By: Claude Opus 5 --- .github/workflows/build.yml | 80 ++++++++++++++++++++++++++ CLAUDE.md | 12 +++- README.md | 42 +++++++++----- _k8s/overlays/dev/commit-patch.yaml | 15 +++++ _k8s/overlays/dev/httproute-patch.yaml | 2 +- _k8s/overlays/dev/kustomization.yaml | 4 +- _k8s/overlays/prod/commit-patch.yaml | 15 +++++ _k8s/overlays/prod/kustomization.yaml | 4 +- app.py | 3 + templates/index.html | 11 ++++ 10 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 _k8s/overlays/dev/commit-patch.yaml create mode 100644 _k8s/overlays/prod/commit-patch.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42a28e8..fffb639 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,10 @@ on: branches: - main - dev + # Filet de sécurité anti-boucle : le commit du job "deploy" ne touche que ce fichier. + # (Un push fait avec GITHUB_TOKEN ne déclenche déjà aucun workflow.) + paths-ignore: + - '_k8s/overlays/*/commit-patch.yaml' pull_request: branches: - main @@ -81,3 +85,79 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + deploy: + name: Trigger ArgoCD redeploy + needs: build + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + permissions: + contents: write + concurrency: + group: deploy-${{ github.ref_name }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: Résoudre l'overlay cible + id: overlay + run: | + case "${{ github.ref_name }}" in + main) echo "path=_k8s/overlays/prod" >> "$GITHUB_OUTPUT" ;; + dev) echo "path=_k8s/overlays/dev" >> "$GITHUB_OUTPUT" ;; + *) echo "Branche non gérée : ${{ github.ref_name }}" >&2; exit 1 ;; + esac + + - name: Écrire l'ENV COMMIT dans le patch kustomize + env: + PATCH: ${{ steps.overlay.outputs.path }}/commit-patch.yaml + BRANCH: ${{ github.ref_name }} + run: | + SHORT_SHA="${GITHUB_SHA::7}" + cat > "$PATCH" < ArgoCD redéploie + # sans changer le tag de l'image. + apiVersion: apps/v1 + kind: Deployment + metadata: + name: demoapp + spec: + template: + spec: + containers: + - name: demoapp + env: + - name: COMMIT + value: "${SHORT_SHA}" + YAML + cat "$PATCH" + + - name: Commit & push (sans redéclencher de workflow) + env: + PATCH: ${{ steps.overlay.outputs.path }}/commit-patch.yaml + BRANCH: ${{ github.ref_name }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git diff --quiet -- "$PATCH"; then + echo "COMMIT déjà à jour, rien à pousser." + exit 0 + fi + + git add "$PATCH" + git commit -m "[skip ci] [Claude] chore: COMMIT=${GITHUB_SHA::7} (${BRANCH})" + + for attempt in 1 2 3; do + if git push origin "HEAD:${BRANCH}"; then + echo "Poussé (tentative ${attempt})." + exit 0 + fi + echo "Push refusé, rebase puis nouvelle tentative..." + git pull --rebase origin "${BRANCH}" + done + echo "Échec du push après 3 tentatives." >&2 + exit 1 diff --git a/CLAUDE.md b/CLAUDE.md index a651dd7..a207d3c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,13 +38,16 @@ cannot serve it. returns 503 so Kubernetes pulls the pod out of rotation. - Container runs as uid 10001 with `readOnlyRootFilesystem: true`. Any code writing to disk breaks the deployment — keep `PYTHONDONTWRITEBYTECODE=1`. -- All config comes from env vars: `REDIS_URL`, `COUNTER_KEY`, `TZ`, `PORT`. No hardcoded values. +- All config comes from env vars: `REDIS_URL`, `COUNTER_KEY`, `TZ`, `PORT`, `COMMIT`. No + hardcoded values. +- `_k8s/overlays/*/commit-patch.yaml` is **CI-generated** — do not hand-edit it, the `deploy` job + overwrites the file on every push. ## Branches and deployment | Branch | Image tag | Overlay | Namespace | Hostname | |---|---|---|---|---| -| `dev` | `:dev` | `_k8s/overlays/dev` | `demoapp-dev` | `dev.demoapp.k8s.lab.ops.nc` | +| `dev` | `:dev` | `_k8s/overlays/dev` | `demoapp-dev` | `demoapp-dev.k8s.lab.ops.nc` | | `main` | `:main` | `_k8s/overlays/prod` | `demoapp` | `demoapp.k8s.lab.ops.nc` | Work on `dev`. Reach `main` through a PR. Hostnames are written literally in each overlay's @@ -68,5 +71,8 @@ Never mark k8s or Docker work done without rendering the manifests or running th ## Known limitations - Redis uses an `emptyDir`: the counter resets when the pod restarts. -- Image tags are mutable per branch, so a new build needs `kubectl rollout restart` to land. +- Image tags are mutable per branch. Redeployment is triggered by the `deploy` job bumping the + `COMMIT` env var, which changes the pod template and makes ArgoCD roll the pods. That commit + must never retrigger CI: it relies on `GITHUB_TOKEN` pushes not firing workflows, plus + `[skip ci]` and `paths-ignore`. Keep all three if you touch the workflow. - `/api/info` returns the server date in the server locale; the browser reformats it in French. diff --git a/README.md b/README.md index f0cae65..d463694 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ compteur de vues persisté dans **Redis** (sans authentification). | `COUNTER_KEY` | `demoapp:views` | Clé du compteur | | `TZ` | `Pacific/Noumea` | Timezone affichée | | `PORT` | `8000` | Port d'écoute | +| `COMMIT` | `unknown` | SHA court affiché sous l'IP ; réécrit par la CI à chaque déploiement | ## Lancer en local @@ -64,8 +65,9 @@ Le workflow `.github/workflows/build.yml` se déclenche sur `dev` et `main` : la branche comme tag** (plus un tag court de SHA) : - branche `dev` → `ghcr.io/ops-nc/demoapp:dev` - branche `main` → `ghcr.io/ops-nc/demoapp:main` +3. **Job `deploy`** — déclenche le redéploiement ArgoCD (voir ci-dessous). -Sur pull request, seul le job `test` est exécuté (pas de push d'image). +Sur pull request, seuls les jobs `test` s'exécutent (ni push d'image, ni commit). --- @@ -78,7 +80,7 @@ deux overlays. Chaque overlay surcharge le hostname de l'HTTPRoute via un patch ``` _k8s/ ├── base/ deployment · service · httproute · redis -├── overlays/dev/ → dev.demoapp.k8s.lab.ops.nc, ns demoapp-dev, image :dev, 1 replica +├── overlays/dev/ → demoapp-dev.k8s.lab.ops.nc, ns demoapp-dev, image :dev, 1 replica ├── overlays/prod/ → demoapp.k8s.lab.ops.nc, ns demoapp, image :main, 2 replicas └── argocd/ Applications ArgoCD prêtes à appliquer ``` @@ -89,7 +91,7 @@ L'HTTPRoute est rattachée à la Gateway existante `main-gateway` ## Vérifier le rendu avant de déployer ```bash -kubectl kustomize _k8s/overlays/dev | grep -A1 hostnames # dev.demoapp.k8s.lab.ops.nc +kubectl kustomize _k8s/overlays/dev | grep -A1 hostnames # demoapp-dev.k8s.lab.ops.nc kubectl kustomize _k8s/overlays/prod | grep -A1 hostnames # demoapp.k8s.lab.ops.nc ``` @@ -99,7 +101,7 @@ Les manifests fournis pointent chacun vers une branche et un overlay différents | Application | Branche suivie | Path | Namespace | URL | |---|---|---|---|---| -| `demoapp-dev` | `dev` | `_k8s/overlays/dev` | `demoapp-dev` | https://dev.demoapp.k8s.lab.ops.nc | +| `demoapp-dev` | `dev` | `_k8s/overlays/dev` | `demoapp-dev` | https://demoapp-dev.k8s.lab.ops.nc | | `demoapp` | `main` | `_k8s/overlays/prod` | `demoapp` | https://demoapp.k8s.lab.ops.nc | ```bash @@ -109,23 +111,35 @@ kubectl apply -f _k8s/argocd/application-prod.yaml Les deux Applications sont en `automated` (prune + selfHeal) avec `CreateNamespace=true`. -## Cycle de déploiement +## Redéploiement automatique (job `deploy`) -1. Push sur `dev` → CI publie `ghcr.io/ops-nc/demoapp:dev` → ArgoCD resynchronise `demoapp-dev`. -2. Merge `dev` → `main` → CI publie `:main` → ArgoCD resynchronise `demoapp`. +Le tag d'image est fixe par branche : un nouveau push ne modifie donc aucun manifest, et ArgoCD +n'a rien à resynchroniser. Le job `deploy` résout ce problème **sans changer de tag** — il réécrit +l'ENV var `COMMIT` du Deployment avec le SHA court dans +`_k8s/overlays//commit-patch.yaml`, puis commite sur la branche courante. -Le tag d'image étant fixe par branche (`imagePullPolicy: Always`), forcer le redéploiement après -un nouveau build se fait via : +Modifier une ENV var change le pod template : ArgoCD voit la dérive, sync, et Kubernetes fait un +rolling update. Comme `imagePullPolicy: Always`, les nouveaux pods tirent l'image fraîche du +même tag. Le SHA déployé est affiché en petit sous l'IP sur la page. + +``` +push sur dev → test → build & push :dev → commit "COMMIT=" → ArgoCD sync → rollout +``` + +**Le commit de la CI ne redéclenche aucun workflow**, via trois protections : + +1. Un push authentifié avec le `GITHUB_TOKEN` ne déclenche pas de workflow (garantie GitHub). +2. Le message de commit porte `[skip ci]`. +3. `on.push.paths-ignore` exclut `_k8s/overlays/*/commit-patch.yaml`. + +Mapping branche → overlay patché : `dev` → `overlays/dev`, `main` → `overlays/prod`. + +Redéploiement manuel si besoin : ```bash kubectl rollout restart deploy/demoapp -n demoapp-dev -# ou, côté ArgoCD -argocd app actions run demoapp-dev restart --kind Deployment ``` -> Pour un suivi automatique des nouvelles images, brancher **ArgoCD Image Updater** ou passer la -> CI en tag immuable (SHA) avec commit du tag dans l'overlay. - ## Notes - Le Redis déployé utilise un `emptyDir` : le compteur repart à zéro si le pod redémarre. diff --git a/_k8s/overlays/dev/commit-patch.yaml b/_k8s/overlays/dev/commit-patch.yaml new file mode 100644 index 0000000..8d4a8d8 --- /dev/null +++ b/_k8s/overlays/dev/commit-patch.yaml @@ -0,0 +1,15 @@ +# Généré par la CI (job "deploy") à chaque push sur dev. +# La modification de cette ENV var change le pod template -> ArgoCD redéploie +# sans changer le tag de l'image. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demoapp +spec: + template: + spec: + containers: + - name: demoapp + env: + - name: COMMIT + value: "initial" diff --git a/_k8s/overlays/dev/httproute-patch.yaml b/_k8s/overlays/dev/httproute-patch.yaml index 4d53fc3..82ba758 100644 --- a/_k8s/overlays/dev/httproute-patch.yaml +++ b/_k8s/overlays/dev/httproute-patch.yaml @@ -4,4 +4,4 @@ metadata: name: demoapp spec: hostnames: - - dev.demoapp.k8s.lab.ops.nc + - demoapp-dev.k8s.lab.ops.nc diff --git a/_k8s/overlays/dev/kustomization.yaml b/_k8s/overlays/dev/kustomization.yaml index 5206668..284d140 100644 --- a/_k8s/overlays/dev/kustomization.yaml +++ b/_k8s/overlays/dev/kustomization.yaml @@ -15,6 +15,8 @@ replicas: - name: demoapp count: 1 -# hostname de l'HTTPRoute : dev.demoapp.k8s.lab.ops.nc +# httproute-patch : hostname demoapp-dev.k8s.lab.ops.nc +# commit-patch : ENV COMMIT, réécrit par la CI pour forcer le redéploiement ArgoCD patches: - path: httproute-patch.yaml + - path: commit-patch.yaml diff --git a/_k8s/overlays/prod/commit-patch.yaml b/_k8s/overlays/prod/commit-patch.yaml new file mode 100644 index 0000000..d06789e --- /dev/null +++ b/_k8s/overlays/prod/commit-patch.yaml @@ -0,0 +1,15 @@ +# Généré par la CI (job "deploy") à chaque push sur main. +# La modification de cette ENV var change le pod template -> ArgoCD redéploie +# sans changer le tag de l'image. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demoapp +spec: + template: + spec: + containers: + - name: demoapp + env: + - name: COMMIT + value: "initial" diff --git a/_k8s/overlays/prod/kustomization.yaml b/_k8s/overlays/prod/kustomization.yaml index c9638cd..85178f5 100644 --- a/_k8s/overlays/prod/kustomization.yaml +++ b/_k8s/overlays/prod/kustomization.yaml @@ -15,6 +15,8 @@ replicas: - name: demoapp count: 2 -# hostname de l'HTTPRoute : demoapp.k8s.lab.ops.nc +# httproute-patch : hostname demoapp.k8s.lab.ops.nc +# commit-patch : ENV COMMIT, réécrit par la CI pour forcer le redéploiement ArgoCD patches: - path: httproute-patch.yaml + - path: commit-patch.yaml diff --git a/app.py b/app.py index 5934bb0..55be1af 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,7 @@ REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") COUNTER_KEY = os.environ.get("COUNTER_KEY", "demoapp:views") TZ_NAME = os.environ.get("TZ", "Pacific/Noumea") +COMMIT = os.environ.get("COMMIT", "unknown") app = Flask(__name__) redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True) @@ -61,6 +62,7 @@ def index(): hostname=hostname, ip=ip, views=views, + commit=COMMIT, ) @@ -77,6 +79,7 @@ def api_info(): hostname=hostname, ip=ip, views=get_views(), + commit=COMMIT, ) diff --git a/templates/index.html b/templates/index.html index 01aeecb..89fa61f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -135,6 +135,16 @@ word-break: break-all; } + .commit { + margin-top: 0.45rem; + font-size: 0.68rem; + letter-spacing: 0.04em; + color: #6b748c; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + } + + .commit span { color: #97a1b8; } + .badge { display: inline-block; margin-top: 0.9rem; @@ -168,6 +178,7 @@

DemoApp

Machine
{{ hostname }}
{{ ip }}
+
commit {{ commit }}
Adresse IP interne From 5148b4e304d96b6b31dc5b26cdb7bedac7109fa8 Mon Sep 17 00:00:00 2001 From: valentin2105 Date: Thu, 6 Aug 2026 19:59:00 +1100 Subject: [PATCH 2/5] [Claude] docs: note sur le marqueur de skip CI dans les messages de commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub scanne tout le message (sujet + corps) : écrire le marqueur littéralement dans un message empêche silencieusement tout run CI. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index a207d3c..14450c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,4 +75,11 @@ Never mark k8s or Docker work done without rendering the manifests or running th `COMMIT` env var, which changes the pod template and makes ArgoCD roll the pods. That commit must never retrigger CI: it relies on `GITHUB_TOKEN` pushes not firing workflows, plus `[skip ci]` and `paths-ignore`. Keep all three if you touch the workflow. + +## Gotcha: commit messages + +GitHub scans the whole commit message — subject *and* body — for the skip marker (`skip ci` +in square brackets). Never write that literal sequence in a commit message when describing +this mechanism, or your push silently produces no CI run. Referring to it inside files +(README, workflow YAML) is safe; only the commit message matters. - `/api/info` returns the server date in the server locale; the browser reformats it in French. From cfa7041b83bbf3c79b7519ee1d47799a305c49f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:59:59 +0000 Subject: [PATCH 3/5] [skip ci] [Claude] chore: COMMIT=5148b4e (dev) --- _k8s/overlays/dev/commit-patch.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_k8s/overlays/dev/commit-patch.yaml b/_k8s/overlays/dev/commit-patch.yaml index 8d4a8d8..8dc8f41 100644 --- a/_k8s/overlays/dev/commit-patch.yaml +++ b/_k8s/overlays/dev/commit-patch.yaml @@ -12,4 +12,4 @@ spec: - name: demoapp env: - name: COMMIT - value: "initial" + value: "5148b4e" From 2c008d5941699a1f54006731cc184d5df8786599 Mon Sep 17 00:00:00 2001 From: valentin2105 Date: Thu, 6 Aug 2026 20:03:44 +1100 Subject: [PATCH 4/5] =?UTF-8?q?[Claude]=20fix:=20no-store=20sur=20/=20pour?= =?UTF-8?q?=20que=20le=20compteur=20s'incr=C3=A9mente=20=C3=A0=20chaque=20?= =?UTF-8?q?visite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sans en-tête de cache, un navigateur peut resservir la page depuis son cache et l'INCR Redis n'a pas lieu. Co-Authored-By: Claude Opus 5 --- app.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 55be1af..0b72de5 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ import redis from asgiref.wsgi import WsgiToAsgi -from flask import Flask, jsonify, render_template +from flask import Flask, jsonify, make_response, render_template REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") COUNTER_KEY = os.environ.get("COUNTER_KEY", "demoapp:views") @@ -53,7 +53,7 @@ def index(): hostname, ip = get_host_info() time_str, date_str, tz_label, offset = get_time() views = incr_views() - return render_template( + page = render_template( "index.html", time=time_str, date=date_str, @@ -64,6 +64,10 @@ def index(): views=views, commit=COMMIT, ) + # Le compteur doit s'incrémenter à chaque visite : pas de cache navigateur. + response = make_response(page) + response.headers["Cache-Control"] = "no-store" + return response @app.route("/api/info") From 65e5a512b31fa47e70a6401abfd1f5bf253971d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:04:49 +0000 Subject: [PATCH 5/5] [skip ci] [Claude] chore: COMMIT=2c008d5 (dev) --- _k8s/overlays/dev/commit-patch.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_k8s/overlays/dev/commit-patch.yaml b/_k8s/overlays/dev/commit-patch.yaml index 8dc8f41..6e981eb 100644 --- a/_k8s/overlays/dev/commit-patch.yaml +++ b/_k8s/overlays/dev/commit-patch.yaml @@ -12,4 +12,4 @@ spec: - name: demoapp env: - name: COMMIT - value: "5148b4e" + value: "2c008d5"