diff --git a/README.md b/README.md index e69de29..b05f4cc 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,48 @@ +# RF_ACTIONS + +Actions Gitea partagées par les projets de Raggaroth Factory. + +Ce dépôt est **public parce qu'il doit l'être** : `act_runner` télécharge les +actions appelées par `uses:` sans s'authentifier, et un dépôt privé lui répond +« repository not found ». Il ne contient donc que le code des actions — rien +qui décrive l'infrastructure qui les exécute. Cette description, les décisions +et la procédure d'installation du runner vivent dans RF_CICD, qui reste privé. + +**Règle qui en découle** : rien de propre à une machine, un compte ou un chemin +de l'infrastructure n'entre ici. Et aucun secret, jamais — ils arrivent par les +entrées des actions, depuis les secrets du dépôt appelant. + +## `build-push` + +Construit une image avec **buildah**, sans démon ni socket, et la publie dans +le registre de Gitea sous les tags de sa filière. **Ne déploie rien.** + +```yaml +- uses: https://gitea.raggaroth-factory.fr/public/RF_ACTIONS/build-push@main + with: + image: gitea.raggaroth-factory.fr/raggaroth-factory/ + dockerfile: Dockerfile + context: . + registry_user: ${{ secrets.REGISTRY_USER }} + registry_password: ${{ secrets.REGISTRY_PASSWORD }} +``` + +| Déclencheur | Tags publiés | +|---|---| +| poussée sur `develop` | `:dev` et `:` | +| tag `vX.Y.Z` (ou `vX.Y.Z-rc.1`) | `:vX.Y.Z`, orthographié comme le tag git, et rien d'autre | +| toute autre référence | **échec**, sans rien construire | + +Sorties : `tags` (les tags publiés) et `digest` (l'empreinte `sha256:…` de +l'image publiée). + +**Exigences** : l'image du conteneur de job doit porter `buildah`. L'action +construit avec `--storage-driver vfs --isolation chroot`, ce qui fonctionne +dans un conteneur non privilégié, sans capacité ajoutée ni `/dev/fuse`. Les +`FROM` des Dockerfiles construits doivent être des **noms complets** +(`docker.io/library/nginx:alpine`, pas `nginx:alpine`) : buildah n'a pas de +registre de recherche par défaut, et sa table d'alias est incomplète. + +**Toujours `uses:` en URL complète** : la forme courte `public/RF_ACTIONS/…` +serait résolue contre le serveur d'actions par défaut de l'instance, qui +n'est pas forcément celle-ci. diff --git a/build-push/action.yml b/build-push/action.yml new file mode 100644 index 0000000..9730a81 --- /dev/null +++ b/build-push/action.yml @@ -0,0 +1,161 @@ +name: 'Build & push' +description: >- + Construit une image avec buildah, sans démon ni socket, et la publie dans le + registre de Gitea sous les tags de sa filière (ADR 0002 et 0003 de RF_CICD). Ne + déploie rien : la filière dev tire l'image, la production se déploie sur + ordre. Les tests et le lint du projet tournent AVANT cette étape, dans le + workflow appelant ; l'action ne filtre rien elle-même. + +# Tags publiés, selon ce qui a déclenché le workflow : +# +# poussée sur develop → :dev et : +# tag vX.Y.Z → :vX.Y.Z, orthographié comme le tag git, et rien d'autre +# toute autre référence → échec, sans rien construire +# +# Le refus des autres références est voulu : une fusion sur main ne publie +# rien (ADR 0002), et une branche de fonctionnalité qui appellerait l'action +# par erreur ne doit pas pouvoir écraser :dev. Mieux vaut un job rouge qui dit +# pourquoi qu'une image publiée sous un nom que personne n'a choisi. +# +# Il n'y a volontairement aucun tag flottant de production (pas de :prod, pas +# de :latest), et rien n'est élagué. + +inputs: + image: + description: >- + Nom complet de l'image, sans tag — doit commencer par le registre, ex. + gitea.raggaroth-factory.fr/raggaroth-factory/scrum-board-backend + required: true + dockerfile: + description: Chemin du Dockerfile/Containerfile à construire. + required: true + context: + description: Contexte de construction. + required: false + default: '.' + registry: + description: Hôte du registre. + required: false + default: gitea.raggaroth-factory.fr + registry_user: + description: Compte qui publie dans le registre. + required: true + registry_password: + description: Jeton du compte qui publie. Par un secret Gitea Actions, jamais en clair. + required: true + +outputs: + tags: + description: Les tags publiés, séparés par des espaces. + value: ${{ steps.tags.outputs.tags }} + digest: + description: L'empreinte (sha256:…) de l'image publiée — c'est elle que la filière dev compare. + value: ${{ steps.publish.outputs.digest }} + +runs: + using: composite + steps: + # Les entrées passent TOUJOURS par l'environnement, jamais par « ${{ }} » + # dans le script : une expression est recopiée telle quelle dans le texte + # bash avant son exécution, et une valeur contenant « "; … » deviendrait du + # code. L'ancienne version interpolait ainsi le mot de passe du registre + # dans un « echo ». + - name: Tags de la filière + id: tags + shell: bash + env: + INPUT_IMAGE: ${{ inputs.image }} + INPUT_REGISTRY: ${{ inputs.registry }} + run: | + set -euo pipefail + + case "$INPUT_IMAGE" in + "$INPUT_REGISTRY"/*) ;; + *) echo "::error::l'image « $INPUT_IMAGE » n'est pas dans le registre « $INPUT_REGISTRY » : les identifiants seraient envoyés au mauvais hôte, ou l'image poussée sans eux" + exit 1 ;; + esac + + # GITHUB_REF et non GITHUB_REF_NAME : seul le premier distingue une + # branche « v1 » d'un tag « v1 ». + case "$GITHUB_REF" in + refs/heads/develop) + TAGS="dev ${GITHUB_SHA:0:12}" ;; + refs/tags/v*) + VERSION=${GITHUB_REF#refs/tags/} + # Un tag d'image n'accepte que [A-Za-z0-9_.-] : on exige une + # version lisible plutôt que de corriger en silence ce qui a été + # posé, pour que le tag git et le tag d'image restent identiques. + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then + echo "::error::tag « $VERSION » : attendu vX.Y.Z (ou vX.Y.Z-rc.1)" + exit 1 + fi + TAGS="$VERSION" ;; + *) + echo "::error::« $GITHUB_REF » ne publie rien : seules une poussée sur develop et un tag vX.Y.Z publient (ADR 0002)" + exit 1 ;; + esac + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + echo "Tags : $TAGS" + + - name: Construire et publier + id: publish + shell: bash + env: + INPUT_IMAGE: ${{ inputs.image }} + INPUT_DOCKERFILE: ${{ inputs.dockerfile }} + INPUT_CONTEXT: ${{ inputs.context }} + INPUT_REGISTRY: ${{ inputs.registry }} + INPUT_TAGS: ${{ steps.tags.outputs.tags }} + REGISTRY_USER: ${{ inputs.registry_user }} + REGISTRY_PASSWORD: ${{ inputs.registry_password }} + run: | + set -euo pipefail + + if ! command -v buildah >/dev/null; then + echo "::error::buildah absent de l'image de job : le runner doit exécuter ce job dans une image qui le porte (voir README de RF_ACTIONS)" + exit 1 + fi + + # « --storage-driver vfs » sur CHAQUE appel, et pas par la variable + # STORAGE_DRIVER : constaté le 2026-10-07 sur buildah 1.33.7, avec la + # seule variable, un « build » a laissé un stockage overlay que l'appel + # suivant a refusé d'ouvrir (« 'overlay' is not supported over + # overlayfs »). Avec le drapeau explicite, build, images et push + # passent. vfs est le seul pilote qui tienne + # dans un conteneur de job sans privilège ni /dev/fuse (ADR 0003). + b() { buildah --storage-driver vfs "$@"; } + + # Fichier d'authentification explicite, dans le répertoire temporaire du + # 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. + export REGISTRY_AUTH_FILE="${RUNNER_TEMP:-/tmp}/build-push-auth.json" + trap 'rm -f "$REGISTRY_AUTH_FILE"' EXIT + + # printf est une primitive du shell : le jeton ne passe dans l'argv + # d'aucun processus, donc pas dans /proc//cmdline. + printf '%s' "$REGISTRY_PASSWORD" \ + | b login --username "$REGISTRY_USER" --password-stdin "$INPUT_REGISTRY" + + TAG_ARGS=() + for TAG in $INPUT_TAGS; do TAG_ARGS+=(--tag "$INPUT_IMAGE:$TAG"); done + + # --isolation chroot : pas d'espace de noms imbriqué à créer pour les + # RUN, c'est ce qui permet de construire sans privilège (ADR 0003). + # Les étiquettes OCI disent de quel commit vient l'image : c'est ce que + # lira un « deployer.sh --constat » pour dire ce qui tourne. + b build \ + --isolation chroot \ + --file "$INPUT_DOCKERFILE" \ + --label "org.opencontainers.image.revision=$GITHUB_SHA" \ + --label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ + "${TAG_ARGS[@]}" \ + "$INPUT_CONTEXT" + + DIGEST_FILE="${RUNNER_TEMP:-/tmp}/build-push-digest" + for TAG in $INPUT_TAGS; do + b push --digestfile "$DIGEST_FILE" "$INPUT_IMAGE:$TAG" "docker://$INPUT_IMAGE:$TAG" + echo "Publié : $INPUT_IMAGE:$TAG@$(cat "$DIGEST_FILE")" + done + echo "digest=$(cat "$DIGEST_FILE")" >> "$GITHUB_OUTPUT" + + b logout "$INPUT_REGISTRY" || true