--- # template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml name: Helm Charts # Celui-ci travaille SEUL (pas d'auth Vault, pas d'apply) : on le garde # automatique. Mais `push` sur TOUTES les branches + `pull_request` faisait # partir DEUX runs pour le même commit dès qu'une branche avait une PR. # # Même forme que la CI de kadans : la branche est couverte par `pull_request`, # `main` par le `push` d'après-merge. Un run par événement, aucun angle mort. # # (Le filtre de chemins d'origine, resté en commentaire des années sous un # « gitea don't handle well the paths filter », n'était probablement pas en # cause : il passait par une ancre YAML, et le parseur d'événements de Gitea ne # les résout pas — issues 113 → 117 de kadans. Le job `filter-chart` fait déjà # ce tri au niveau job, donc on n'y retouche pas.) # # ⚠ Chaque clé porte un CORPS explicite : un `pull_request:` nu (valeur nulle) # n'est pas une forme éprouvée sur ce Gitea, et son mode d'échec est le # silencieux — aucun run, aucune erreur. On copie la forme qui tourne (kadans # ci.yml), listes dupliquées à la main, sans ancre. on: workflow_dispatch: {} push: branches: [main] paths-ignore: - '**.md' pull_request: paths-ignore: - '**.md' # cancel any previously-started, yet still active runs of this workflow on the same branch concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true .helm_install_dependencies_sh: &helm_install_dependencies_sh |- # ⚠⚠ MESURÉ le 2026-09-07 (run 6728, étape « RELEVÉ ») : ce runner NE RÉSOUT # PAS `gitea.arcodange.lab` — `getent` code 2, `curl` code 6 — alors qu'il # résout parfaitement les registres publics. SEPT charts sur dix déclarent le # registre Helm interne en dépendance (crowdsec, grafana, hashicorp-vault, # pgbouncer, pgcat, prometheus, redis) : depuis que #32 a énuméré les charts, # leurs jobs mouraient TOUS ici, en 7 s, avant la moindre étape utile — et le # premier à en faire les frais est le premier à avoir été construit. # # Gitea donne au job sa PROPRE adresse joignable dans `GITHUB_SERVER_URL` # (mesuré : http://192.168.65.254:43000). On ne s'en sert qu'en REPLI, et # seulement après avoir constaté que le nom d'origine ne répond pas : rien ne # change sur un runner qui, lui, sait le résoudre. Lecture anonyme du registre # vérifiée (HTTP 200), donc aucun jeton n'entre ici. FORGE_INTERNE='https://gitea.arcodange.lab' joignable() { curl -sf -o /dev/null -m 15 "$1"; } helm_install_dependencies() { chart_file="$1/Chart.yaml" [[ ! -f "$chart_file" ]] && echo "Chart.yaml not found in $1" && return 1 yq eval '.dependencies[]' "$chart_file" -o=json | jq -c '.' | while IFS= read -r dep; do name=$(jq -r '.name' <<< "$dep") version=$(jq -r '.version' <<< "$dep") repo=$(jq -r '.repository' <<< "$dep") en_repli=non if ! joignable "${repo}/index.yaml"; then repli="${repo/$FORGE_INTERNE/${GITHUB_SERVER_URL:-$FORGE_INTERNE}}" if [[ "$repli" != "$repo" ]] && joignable "${repli}/index.yaml"; then echo " ⚠ ${repo} injoignable depuis ce runner — repli sur ${repli}" repo="$repli" en_repli=oui fi fi url=$(curl -s "${repo}/index.yaml" | yq eval ".entries.${name}[] | select(.version == \"${version}\") | .urls[0]" -) # L'index du registre interne publie des URLs ABSOLUES vers le même nom # d'hôte : le repli doit les suivre, sinon on retombe dans le code 6. # ⚠ On ne réécrit QUE si l'on est effectivement en repli — sur un runner # sain, le chemin d'origine reste le chemin d'origine. # ⚠ Un `[[ … ]] && …` ici serait un piège : sous `set -e`, la liste rend # non nul quand la condition est fausse, et le script AVORTE. if [[ "$en_repli" = oui ]]; then url="${url/$FORGE_INTERNE/$GITHUB_SERVER_URL}" fi echo "Dependency: $name, Version: $version, URL: $url" mkdir -p "$1/charts" && curl -sL "$url" -o "$1/charts/${name}-${version}.tgz" done } helm_install_dependencies $chart jobs: filter-chart: name: Detect changed charts runs-on: ubuntu-latest outputs: library_charts: ${{steps.filter-charts.outputs.library_charts}} application_charts: ${{steps.filter-charts.outputs.application_charts}} steps: - uses: actions/checkout@v4 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v45 - name: Filter modified charts id: filter-charts run: | echo "Changed files:" echo "${{ steps.changed-files.outputs.all_changed_files }}" # Find unique directories that contain Chart.yaml among the changed files modified_dirs=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' | xargs -n1 dirname | sort -u || true) # Initialize an array to store directories that contain Chart.yaml helm_chart_dirs=() # Function to find the closest directory containing Chart.yaml find_chart_root() { dir="$1" while [[ "$dir" != "/" && "$dir" != "." ]]; do if [[ -f "$dir/Chart.yaml" ]]; then echo "$dir" return fi dir=$(dirname "$dir") done } # Iterate over each modified directory and find the root chart directory for dir in $modified_dirs; do chart_dir=$(find_chart_root "$dir") if [[ -n "$chart_dir" && ! " ${helm_chart_dirs[*]} " =~ " ${chart_dir} " ]]; then helm_chart_dirs+=("$chart_dir") fi done # Initialize arrays for library and application charts library_dirs=() application_dirs=() # Iterate over each modified directory and check the 'type' field in Chart.yaml for dir in ${helm_chart_dirs[@]}; do chart_type=$(yq eval '.type' "$dir/Chart.yaml" || echo "undefined") # Add directories to corresponding arrays based on the 'type' if [[ "$chart_type" == "library" ]]; then library_dirs+=("$dir") elif [[ "$chart_type" == "application" ]]; then application_dirs+=("$dir") fi done # Convert the arrays to JSON format library_json=$(printf '%s\n' "${library_dirs[@]}" | jq -R . | jq -cs 'map(select(. != ""))') application_json=$(printf '%s\n' "${application_dirs[@]}" | jq -R . | jq -cs 'map(select(. != ""))') # Output the JSON arrays echo "Modified Helm library charts directories: $library_json" echo "library_charts=$library_json" >> $GITHUB_OUTPUT echo "Modified Helm application charts directories: $application_json" echo "application_charts=$application_json" >> $GITHUB_OUTPUT library-charts: &charts-matrix-job name: Library charts ${{ matrix.chart }} runs-on: ubuntu-latest needs: filter-chart strategy: matrix: # ⚠⚠ ÉNUMÉRATION EXHAUSTIVE, ET ELLE DOIT LE RESTER. # # Gitea ne sait pas monter une matrice DYNAMIQUE : on ne peut pas # écrire `fromJson(needs.filter-chart.outputs.library_charts)` ici. # La conséquence est passée inaperçue longtemps — le détecteur # calculait la bonne réponse, et PERSONNE NE LA CONSOMMAIT : le `if:` # ci-dessous vérifie seulement si le chart CODÉ EN DUR figure dans la # liste détectée. Seuls `tool` et `pgcat` pouvaient donc être # construits ; les HUIT AUTRES charts de ce dépôt ne l'étaient jamais, # et leurs PR affichaient vert. Mesuré le 2026-09-02 sur la PR #32, # qui réécrivait le chart `minio` en entier : `Detect changed charts` # ✓, les deux jobs de construction `Skipped`, statut global `success`. # # Le remède n'a pas besoin de matrice dynamique : le `if:` filtre DÉJÀ # sur la sortie du détecteur, donc un chart non modifié reste `Skipped`. # Il ne manquait que l'ÉNUMÉRATION. # # ⚠ Ajouter un chart au dépôt SANS l'ajouter ici le rend invisible à la # CI, en silence. C'est le mode d'échec de ce fichier. chart: [tool] # chart: ${{ fromJson(needs.filter-chart.outputs.library_charts) }} type: [library] if: >- ${{ always() && !contains(needs.*.result, 'failure') && needs.filter-chart.result == 'success' && ( contains(fromJson(needs.filter-chart.outputs.library_charts), matrix.chart) || contains(fromJson(needs.filter-chart.outputs.application_charts), matrix.chart) ) && ( contains(fromJSON('["","pull_request"]'), github.event_name) || github.ref == 'refs/heads/main' ) }} env: chart: ${{ matrix.chart }} steps: - uses: actions/checkout@v4 # ⚠⚠ UNE GATE QUI N'EST CÂBLÉE NULLE PART N'EXISTE PAS (tools#36) — et une # gate placée APRÈS une étape qui échoue toujours n'existe pas non plus : # elle sort `Skipped`, ni rouge ni absente, rien ne dépasse. Le banc du # veilleur passe donc AVANT le téléchargement des dépendances, qui a besoin # du réseau et du registre Helm interne. Il n'a besoin d'aucun des deux : # stdlib Python seule, plus `yq`/`jq` déjà présents. - name: Banc du veilleur (chart prometheus) if: >- ${{ matrix.chart == 'prometheus' && ( contains(fromJSON('["","pull_request"]'), github.event_name) || github.ref != 'refs/heads/main' ) }} run: | set -euo pipefail python3 -m unittest discover -s prometheus/veilleur -v # Le témoin toujours allumé ne doit JAMAIS partir sur Telegram : sans # sa route vers le récepteur vide il sonnerait toutes les 3 h, et un # témoin qui sonne est un témoin qu'on coupe. yq -o=json eval '.prometheus.alertmanager.config' prometheus/values.yaml > /tmp/am.json jq -e '[.route.routes[] | select(.matchers[0] == "alertname=\"Veilleur\"") | .receiver] == ["neant"]' /tmp/am.json jq -e '[.receivers[] | select(.name == "neant") | keys] == [["name"]]' /tmp/am.json # ⚠ RELEVÉ, PAS UNE GATE — il ne peut pas faire échouer le job. Le premier # run du job `Application charts prometheus` (le tout premier depuis que # #32 a énuméré les charts) est mort ICI, `curl` code 6 : le runner ne # RÉSOUT PAS l'hôte de la dépendance. Sept charts sur dix dépendent du # registre Helm interne `gitea.arcodange.lab` — donc sept jobs sur dix ne # peuvent pas passer, quel que soit leur contenu. Ce relevé nomme lequel # des deux hôtes tombe, pour que la prochaine session n'ait pas à le # deviner. À retirer quand le trou est bouché. - name: RELEVÉ — ce que le runner sait résoudre run: | set +e echo "GITHUB_SERVER_URL=${GITHUB_SERVER_URL:-}" for hote in gitea.arcodange.lab prometheus-community.github.io; do echo "--- $hote ---" getent hosts "$hote" || echo " PAS DE RÉSOLUTION (getent code $?)" curl -sS -o /dev/null -m 10 -w " http=%{http_code}\n" "https://$hote/" \ || echo " curl code $?" done exit 0 - run: *helm_install_dependencies_sh - name: Install Helm for test if: >- ${{ matrix.type != 'library' && ( contains(fromJSON('["","pull_request"]'), github.event_name) || github.ref != 'refs/heads/main' ) }} uses: azure/setup-helm@v4 - name: Helm template if: >- ${{ matrix.type != 'library' && ( contains(fromJSON('["","pull_request"]'), github.event_name) || github.ref != 'refs/heads/main' ) }} run: helm template $chart --debug - name: publish ${{ matrix.chart }} helm chart if: ${{ contains(fromJSON('["","push"]'), github.event_name) && github.ref == 'refs/heads/main' }} run: | set -x chart=${chart:-tool} chart_version=`yq eval .version ${chart}/Chart.yaml` chart_package=${chart}-${chart_version}.tgz # helm package ${chart} tar -X ${chart}/.helmignore -czf ${chart_package} ${chart} # ⚠⚠ LE MÊME REPLI QUE POUR LA DESCENTE DES DÉPENDANCES, ET C'EST # POURQUOI CE COMMENTAIRE EXISTE. Le correctif du 2026-09-07 n'avait # posé le repli QUE sur `helm_install_dependencies` ; celui-ci est # resté sur le nom d'origine, et `main` est devenu rouge dès la # fusion suivante — `curl` code 6, « couldn't resolve host ». # **Un remède qui vit à DEUX endroits ne se propage pas par la # vigilance.** Si un troisième appel au registre apparaît un jour, il # a besoin de ces trois lignes lui aussi. FORGE_INTERNE='https://gitea.arcodange.lab' registre="${FORGE_INTERNE}/api/packages/${{ github.repository_owner }}/helm" if ! curl -sf -o /dev/null -m 15 "${registre}/index.yaml"; then repli="${registre/$FORGE_INTERNE/${GITHUB_SERVER_URL:-$FORGE_INTERNE}}" if [[ "$repli" != "$registre" ]] && curl -sf -o /dev/null -m 15 "${repli}/index.yaml"; then echo " ⚠ ${registre} injoignable depuis ce runner — repli sur ${repli}" registre="$repli" fi fi # ⚠ ICI LE JETON PART, alors que la descente est anonyme. Le repli est # `http://192.168.65.254:43000` — la passerelle Docker de l'hôte, en # clair. Ce n'est pas une exposition NEUVE : `actions/checkout` pose # déjà son `http..extraheader` porteur du jeton de forge à # chaque job. Même chemin, même machine, même classe de risque. # ⚠⚠ ET L'APPEL DOIT POUVOIR ÉCHOUER. `curl` rend 0 sur un 401 comme # sur un 409 : l'étape passait au vert en ne publiant rien. On relève # donc le CODE HTTP (`-w`) et on en décide — le code de sortie de # `curl`, lui, ne parle que du transport. Mesuré le 2026-09-07 : le # registre ne contenait que `minio 0.2.0` et `tool 0.1.0`, deux # charts sur dix, sans que rien n'ait jamais rougi. # ⚠ `409` est ACCEPTÉ et nommé : republier une version inchangée n'est # pas une faute, c'est le cas normal d'un lot qui ne touche pas au # `version:` du chart. Tout autre code arrête le job. code=$(curl -s -o /tmp/publication.out -w '%{http_code}' \ --user ${{ github.actor }}:${{ secrets.PACKAGES_TOKEN }} \ -X POST --upload-file ./${chart_package} \ "${registre}/api/charts") || { echo "✖ curl a échoué (code $?) — le registre est injoignable"; exit 1; } cat /tmp/publication.out || true case "$code" in 2??) echo "✓ ${chart_package} publié (HTTP $code)" ;; 409) echo "✓ ${chart_package} déjà présent au registre (HTTP 409) — rien à republier" ;; *) echo "✖ publication REFUSÉE (HTTP $code) — le chart n'est PAS au registre"; exit 1 ;; esac application-charts: <<: *charts-matrix-job name: Application charts ${{ matrix.chart }} needs: [filter-chart,library-charts] strategy: matrix: # ⚠ Les NEUF charts applicatifs du dépôt — voir l'avertissement du job # `library-charts` ci-dessus. Un chart absent de cette liste n'est # JAMAIS construit, et sa PR est verte quand même. # chart: ${{ fromJson(needs.filter-chart.outputs.application_charts) }} chart: [chart, crowdsec, grafana, hashicorp-vault, minio, pgbouncer, pgcat, prometheus, redis] type: [application]