🔒️ build-push construit et scanne sur toute référence, PR comprise ; il ne publie que sur develop et les tags (raggaroth-factory/RF_CICD#7)

Demande de Benjamin, 2026-10-08 : « il faut que le scan se fasse au build
pour empêcher le build ». Jusqu'ici, le scan ne tournait que dans la
publication, après la fusion : une PR n'était jamais scannée, et une
dépendance vulnérable passait la revue.

- Toute référence : construction + scan bloquant. Seules develop et
  vX.Y.Z publient ; une PR, une autre branche ou main s'arrêtent après le
  scan, sous un tag local « controle-<sha> » jamais poussé.
- Identifiants du registre facultatifs : lus et exigés seulement si la
  référence publie. Une PR ne se connecte pas au registre.
- REGISTRY_AUTH_FILE n'est exporté que pour publier : pointé sur un
  fichier absent, il faisait échouer buildah jusque dans l'export local
  vers le scan (constaté au banc, « credential file is not accessible »).
- Sortie « publie » ; « tags » vide quand rien n'est publié.

Banc, 2026-10-08, les deux étapes extraites du YAML et enchaînées :
  A. PR, sans identifiants, saine      → code 0, registre vide
  B. PR, sans identifiants, vulnérable → code 1, image refusée
  C. develop, identifiants             → :dev et :<sha> publiés
  D. develop, sans identifiants        → code 1, erreur explicite
  E. main                              → code 0, rien publié

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyxLNGHojdXeRMQgWFgzRV
This commit is contained in:
2026-10-08 13:20:09 +02:00
co-authored by Claude Opus 5.5
parent 9d88bc3639
commit 50e0e9e258
2 changed files with 97 additions and 46 deletions
+30 -16
View File
@@ -14,27 +14,41 @@ entrées des actions, depuis les secrets du dépôt appelant.
## `build-push` ## `build-push`
Construit une image avec **buildah**, sans démon ni socket, et la publie dans Construit une image avec **buildah**, sans démon ni socket, la passe au **scan
le registre de Gitea sous les tags de sa filière. **Ne déploie rien.** de vulnérabilités bloquant**, puis — sur `develop` ou un tag `vX.Y.Z`
seulement — la publie dans le registre de Gitea. **Ne déploie rien.**
**L'image est construite et scannée sur toute référence, PR comprise** : une
image vulnérable fait échouer le pipeline **avant** la fusion, pas après. Le
job qui l'appelle tourne donc sur chaque événement, juste après les tests :
```yaml ```yaml
- uses: https://gitea.raggaroth-factory.fr/public/RF_ACTIONS/build-push@main image:
with: needs: tests
image: gitea.raggaroth-factory.fr/raggaroth-factory/<nom-de-l-image> runs-on: docker
dockerfile: Dockerfile steps:
context: . - uses: actions/checkout@<empreinte de commit> # vX.Y.Z
registry_user: ${{ secrets.REGISTRY_USER }} - uses: https://gitea.raggaroth-factory.fr/public/RF_ACTIONS/build-push@v1.0.0
registry_password: ${{ secrets.REGISTRY_PASSWORD }} with:
image: gitea.raggaroth-factory.fr/raggaroth-factory/<nom-de-l-image>
dockerfile: Dockerfile
context: .
registry_user: ${{ secrets.REGISTRY_USER }}
registry_password: ${{ secrets.REGISTRY_PASSWORD }}
``` ```
| Déclencheur | Tags publiés | | Référence | Construction + scan | Publication |
|---|---| |---|---|---|
| poussée sur `develop` | `:dev` et `:<sha court, 12 caractères>` | | poussée sur `develop` | oui | `:dev` et `:<sha court, 12 caractères>` |
| tag `vX.Y.Z` (ou `vX.Y.Z-rc.1`) | `:vX.Y.Z`, orthographié comme le tag git, et rien d'autre | | tag `vX.Y.Z` (ou `vX.Y.Z-rc.1`) | oui | `:vX.Y.Z`, orthographié comme le tag git, et rien d'autre |
| toute autre référence | **échec**, sans rien construire | | PR, autre branche, `main` | oui | **rien** |
Sorties : `tags` (les tags publiés) et `digest` (l'empreinte `sha256:…` de Les identifiants du registre ne sont lus **que** si la référence publie : une PR
l'image publiée). ne se connecte pas au registre. Ils sont exigés dans ce cas, et leur absence est
une erreur explicite.
Sorties : `publie` (`true` / `false`), `tags` (les tags publiés, vide sinon) et
`digest` (l'empreinte `sha256:…` de l'image publiée).
**Scan de vulnérabilités bloquant.** Entre la construction et la publication, **Scan de vulnérabilités bloquant.** Entre la construction et la publication,
**Trivy** examine l'image. Une CVE **HIGH ou CRITICAL pour laquelle un **Trivy** examine l'image. Une CVE **HIGH ou CRITICAL pour laquelle un
+67 -30
View File
@@ -1,21 +1,23 @@
name: 'Build & push' name: 'Build, scan & push'
description: >- description: >-
Construit une image avec buildah, sans démon ni socket, et la publie dans le Construit une image avec buildah, sans démon ni socket, la passe au scan de
registre de Gitea sous les tags de sa filière (ADR 0002 et 0003 de RF_CICD). Ne vulnérabilités BLOQUANT, et — sur develop ou un tag vX.Y.Z seulement — la
déploie rien : la filière dev tire l'image, la production se déploie sur publie dans le registre de Gitea (ADR 0002 et 0003 de RF_CICD). Ne déploie
ordre. Les tests et le lint du projet tournent AVANT cette étape, dans le rien : la filière dev tire l'image, la production se déploie sur ordre.
workflow appelant ; l'action ne filtre rien elle-même.
# Tags publiés, selon ce qui a déclenché le workflow : # Sur TOUTE référence — PR comprise — l'image est construite et scannée : une
# CVE HIGH ou CRITICAL corrigeable fait échouer le job, avant toute revue de
# fusion (RF_CICD#7, demande de Benjamin du 2026-10-08 : « le scan doit se faire
# au build pour empêcher le build »). Ensuite seulement, selon la référence :
# #
# poussée sur develop → :dev et :<sha court, 12 caractères> # poussée sur develop → publie :dev et :<sha court, 12 caractères>
# tag vX.Y.Z → :vX.Y.Z, orthographié comme le tag git, et rien d'autre # tag vX.Y.Z → publie :vX.Y.Z, orthographié comme le tag git
# toute autre référence → échec, sans rien construire # toute autre référence → ne publie RIEN : construction et scan seulement
# #
# Le refus des autres références est voulu : une fusion sur main ne publie # Une PR, une branche de fonctionnalité, une poussée sur main (ADR 0002) ne
# rien (ADR 0002), et une branche de fonctionnalité qui appellerait l'action # publient donc jamais : elles ne peuvent ni écraser :dev, ni produire une image
# par erreur ne doit pas pouvoir écraser :dev. Mieux vaut un job rouge qui dit # sous un nom que personne n'a choisi. Et elles n'ont pas besoin des
# pourquoi qu'une image publiée sous un nom que personne n'a choisi. # identifiants du registre — l'action ne les demande que pour publier.
# #
# Il n'y a volontairement aucun tag flottant de production (pas de :prod, pas # Il n'y a volontairement aucun tag flottant de production (pas de :prod, pas
# de :latest), et rien n'est élagué. # de :latest), et rien n'est élagué.
@@ -38,16 +40,23 @@ inputs:
required: false required: false
default: gitea.raggaroth-factory.fr default: gitea.raggaroth-factory.fr
registry_user: registry_user:
description: Compte qui publie dans le registre. description: Compte qui publie dans le registre. Exigé seulement quand la référence publie.
required: true required: false
default: ''
registry_password: registry_password:
description: Jeton du compte qui publie. Par un secret Gitea Actions, jamais en clair. description: >-
required: true Jeton du compte qui publie. Par un secret Gitea Actions, jamais en clair.
Exigé seulement quand la référence publie.
required: false
default: ''
outputs: outputs:
tags: tags:
description: Les tags publiés, séparés par des espaces. description: Les tags publiés, séparés par des espaces (vide si rien n'est publié).
value: ${{ steps.tags.outputs.tags }} value: ${{ steps.publish.outputs.tags }}
publie:
description: « true » si l'image a été publiée, « false » si elle a seulement été construite et scannée.
value: ${{ steps.tags.outputs.publier }}
digest: digest:
description: L'empreinte (sha256:…) de l'image publiée — c'est elle que la filière dev compare. description: L'empreinte (sha256:…) de l'image publiée — c'est elle que la filière dev compare.
value: ${{ steps.publish.outputs.digest }} value: ${{ steps.publish.outputs.digest }}
@@ -77,6 +86,7 @@ runs:
# GITHUB_REF et non GITHUB_REF_NAME : seul le premier distingue une # GITHUB_REF et non GITHUB_REF_NAME : seul le premier distingue une
# branche « v1 » d'un tag « v1 ». # branche « v1 » d'un tag « v1 ».
PUBLIER=true
case "$GITHUB_REF" in case "$GITHUB_REF" in
refs/heads/develop) refs/heads/develop)
TAGS="dev ${GITHUB_SHA:0:12}" ;; TAGS="dev ${GITHUB_SHA:0:12}" ;;
@@ -91,13 +101,19 @@ runs:
fi fi
TAGS="$VERSION" ;; TAGS="$VERSION" ;;
*) *)
echo "::error::« $GITHUB_REF » ne publie rien : seules une poussée sur develop et un tag vX.Y.Z publient (ADR 0002)" # Un tag LOCAL, jamais poussé : il ne sert qu'à nommer l'image le
exit 1 ;; # temps de la scanner.
TAGS="controle-${GITHUB_SHA:0:12}"; PUBLIER=false ;;
esac esac
echo "tags=$TAGS" >> "$GITHUB_OUTPUT" echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
echo "Tags : $TAGS" echo "publier=$PUBLIER" >> "$GITHUB_OUTPUT"
if [ "$PUBLIER" = true ]; then
echo "« $GITHUB_REF » : construction, scan, puis publication de : $TAGS"
else
echo "« $GITHUB_REF » : construction et scan seulement — cette référence ne publie rien (ADR 0002)"
fi
- name: Construire et publier - name: Construire, scanner, publier
id: publish id: publish
shell: bash shell: bash
env: env:
@@ -106,6 +122,7 @@ runs:
INPUT_CONTEXT: ${{ inputs.context }} INPUT_CONTEXT: ${{ inputs.context }}
INPUT_REGISTRY: ${{ inputs.registry }} INPUT_REGISTRY: ${{ inputs.registry }}
INPUT_TAGS: ${{ steps.tags.outputs.tags }} INPUT_TAGS: ${{ steps.tags.outputs.tags }}
PUBLIER: ${{ steps.tags.outputs.publier }}
REGISTRY_USER: ${{ inputs.registry_user }} REGISTRY_USER: ${{ inputs.registry_user }}
REGISTRY_PASSWORD: ${{ inputs.registry_password }} REGISTRY_PASSWORD: ${{ inputs.registry_password }}
run: | run: |
@@ -130,14 +147,27 @@ runs:
# Fichier d'authentification explicite, dans le répertoire temporaire du # Fichier d'authentification explicite, dans le répertoire temporaire du
# job, et effacé en sortie quoi qu'il arrive. Le conteneur est jeté à la # job, et effacé en sortie quoi qu'il arrive. Le conteneur est jeté à la
# fin du job, mais un jeton n'a pas à lui survivre d'une étape. # fin du job, mais un jeton n'a pas à lui survivre d'une étape.
export REGISTRY_AUTH_FILE="${RUNNER_TEMP:-/tmp}/build-push-auth.json" FICHIER_AUTH="${RUNNER_TEMP:-/tmp}/build-push-auth.json"
IMAGE_OCI="${RUNNER_TEMP:-/tmp}/build-push-image-oci" IMAGE_OCI="${RUNNER_TEMP:-/tmp}/build-push-image-oci"
trap 'rm -rf -- "$REGISTRY_AUTH_FILE" "$IMAGE_OCI"' EXIT trap 'rm -rf -- "$FICHIER_AUTH" "$IMAGE_OCI"' EXIT
# Connexion au registre SEULEMENT si la référence publie : une PR n'a
# pas à manipuler le jeton de publication, ni même à le recevoir.
# printf est une primitive du shell : le jeton ne passe dans l'argv # printf est une primitive du shell : le jeton ne passe dans l'argv
# d'aucun processus, donc pas dans /proc/<pid>/cmdline. # d'aucun processus, donc pas dans /proc/<pid>/cmdline.
printf '%s' "$REGISTRY_PASSWORD" \ # REGISTRY_AUTH_FILE n'est exporté QUE si l'on publie : pointé sur un
| b login --username "$REGISTRY_USER" --password-stdin "$INPUT_REGISTRY" # fichier inexistant, il fait échouer buildah jusque dans l'export local
# de l'image vers le scan (constaté le 2026-10-08, « credential file is
# not accessible »).
if [ "$PUBLIER" = true ]; then
export REGISTRY_AUTH_FILE="$FICHIER_AUTH"
if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then
echo "::error::cette référence publie, mais registry_user / registry_password sont vides — secrets REGISTRY_USER / REGISTRY_PASSWORD du dépôt ?"
exit 1
fi
printf '%s' "$REGISTRY_PASSWORD" \
| b login --username "$REGISTRY_USER" --password-stdin "$INPUT_REGISTRY"
fi
TAG_ARGS=() TAG_ARGS=()
for TAG in $INPUT_TAGS; do TAG_ARGS+=(--tag "$INPUT_IMAGE:$TAG"); done for TAG in $INPUT_TAGS; do TAG_ARGS+=(--tag "$INPUT_IMAGE:$TAG"); done
@@ -199,10 +229,16 @@ runs:
echo "::endgroup::" echo "::endgroup::"
if ! trivy image --quiet --skip-db-update --scanners vuln --severity HIGH,CRITICAL \ if ! trivy image --quiet --skip-db-update --scanners vuln --severity HIGH,CRITICAL \
--ignore-unfixed "${IGNORE[@]}" --format table --exit-code 1 --input "$IMAGE_OCI"; then --ignore-unfixed "${IGNORE[@]}" --format table --exit-code 1 --input "$IMAGE_OCI"; then
echo "::error::CVE HIGH ou CRITICAL corrigeable dans l'image : rien n'est publié. Corriger (mettre à jour la base ou le paquet), ou l'accepter sciemment dans .trivyignore.yaml, avec justification et date d'expiration." echo "::error::CVE HIGH ou CRITICAL corrigeable dans l'image : elle est REFUSÉE, et rien n'est publié. Corriger (monter l'empreinte de la base, ou la version du paquet), ou l'accepter sciemment dans .trivyignore.yaml, avec justification et date d'expiration."
exit 1 exit 1
fi fi
echo "Scan : aucune CVE HIGH ou CRITICAL corrigeable — publication autorisée" echo "Scan : aucune CVE HIGH ou CRITICAL corrigeable — image acceptée"
if [ "$PUBLIER" != true ]; then
echo "tags=" >> "$GITHUB_OUTPUT"
echo "Construction et scan réussis — « $GITHUB_REF » ne publie rien."
exit 0
fi
DIGEST_FILE="${RUNNER_TEMP:-/tmp}/build-push-digest" DIGEST_FILE="${RUNNER_TEMP:-/tmp}/build-push-digest"
for TAG in $INPUT_TAGS; do for TAG in $INPUT_TAGS; do
@@ -210,5 +246,6 @@ runs:
echo "Publié : $INPUT_IMAGE:$TAG@$(cat "$DIGEST_FILE")" echo "Publié : $INPUT_IMAGE:$TAG@$(cat "$DIGEST_FILE")"
done done
echo "digest=$(cat "$DIGEST_FILE")" >> "$GITHUB_OUTPUT" echo "digest=$(cat "$DIGEST_FILE")" >> "$GITHUB_OUTPUT"
echo "tags=$INPUT_TAGS" >> "$GITHUB_OUTPUT"
b logout "$INPUT_REGISTRY" || true b logout "$INPUT_REGISTRY" || true