All checks were successful
Le job `sanity-build` (build de `apps/api/Dockerfile` sans push) devient
`build-and-push-images` : `docker login` sur le registre Forgejo intégré
via le token Actions du run (`permissions: packages: write`), puis build +
push des 3 images `batchcooking-{app,intent-service,llm-worker}` taguées
`vX.Y.Z` et `latest`. Un job séquentiel plutôt qu'une matrix — 3 builds
concurrents saturent le daemon Docker du runner unique (cf. mémoire
`ci-forgejo-runner-constraints`) sans gain réel pour une release.
`docker-compose.yml` référence maintenant ces images
(`image: git.dev.kyuno.fr/kyuno/batchcooking-*:${IMAGE_TAG:-latest}`) avec
le bloc `build:` gardé en repli local — Portainer peut tirer les images
pré-construites ou continuer à builder depuis Git. `.env.example` documente
`IMAGE_TAG` ; le README (section registre + Release) est réécrit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
254 lines
12 KiB
YAML
254 lines
12 KiB
YAML
name: Release
|
|
|
|
# On a `vX.Y.Z` tag: build the three service images and push them to this
|
|
# Forgejo instance's built-in container registry (git.dev.kyuno.fr/<owner>/
|
|
# batchcooking-{app,intent-service,llm-worker}, tagged with the release tag
|
|
# and `latest`), build+attach a signed Android APK, publish a Forgejo
|
|
# Release, then (best-effort) ping Portainer's deploy webhook. Portainer can
|
|
# pull those prebuilt images (docker-compose.yml references them via
|
|
# ${IMAGE_TAG}) or keep building from the Git remote — `docker-compose.yml`
|
|
# keeps a `build:` fallback next to every `image:`.
|
|
on:
|
|
push:
|
|
tags: ["v*.*.*"]
|
|
|
|
permissions:
|
|
contents: write
|
|
# Lets this workflow's GITHUB_TOKEN push to the Forgejo container registry
|
|
# (the `build-and-push-images` job below `docker login`s with it). If a
|
|
# future instance policy scopes the run token tighter, swap that login's
|
|
# password for a `write:package` PAT added as a repo secret.
|
|
packages: write
|
|
|
|
jobs:
|
|
# Build the three service images at the tagged commit and push them to the
|
|
# Forgejo container registry. A successful push also *is* the "does it
|
|
# still build" check the old `sanity-build` job did (which only covered
|
|
# apps/api). One sequential job, not a per-image matrix: three concurrent
|
|
# `docker build`s hammer this single persistent runner's Docker daemon
|
|
# (the contended resource per project memory `ci-forgejo-runner-constraints`)
|
|
# for no real gain on a release, which is rare and usually runs alone.
|
|
build-and-push-images:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
# This runner's job containers don't come with docker preinstalled —
|
|
# same reason ci.yml's `test` job has to do this before its own
|
|
# `docker run` calls.
|
|
- name: Install docker
|
|
run: apt-get update && apt-get install -y docker.io
|
|
|
|
- uses: https://github.com/actions/checkout@v4
|
|
|
|
- name: Build et push des 3 images vers le registre Forgejo
|
|
env:
|
|
# The per-run token Forgejo Actions mints — has `packages: write`
|
|
# via this workflow's `permissions:` block. `docker login` accepts
|
|
# it as the password for the actor who pushed the tag.
|
|
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
# $GITHUB_SERVER_URL is "https://git.dev.kyuno.fr" — the registry
|
|
# is that same host. $GITHUB_REPOSITORY is "<owner>/<repo>"; the
|
|
# registry namespace is the owner, lower-cased (Forgejo package
|
|
# names are case-insensitive but the CLI wants a stable form).
|
|
registry="${GITHUB_SERVER_URL#http*://}"
|
|
owner="$(printf '%s' "${GITHUB_REPOSITORY%%/*}" | tr '[:upper:]' '[:lower:]')"
|
|
tag="$GITHUB_REF_NAME"
|
|
echo "$REGISTRY_TOKEN" | docker login "$registry" -u "$GITHUB_ACTOR" --password-stdin
|
|
|
|
# Every Dockerfile's build context is the repo root (see
|
|
# docker-compose.yml's `build.context: .`), hence the trailing `.`.
|
|
build_push() {
|
|
image="$registry/$owner/$1"
|
|
echo "::group::$1"
|
|
docker build -f "$2" -t "$image:$tag" -t "$image:latest" .
|
|
docker push "$image:$tag"
|
|
docker push "$image:latest"
|
|
echo "::endgroup::"
|
|
}
|
|
build_push batchcooking-app apps/api/Dockerfile
|
|
build_push batchcooking-intent-service services/tech-step-intent-service/Dockerfile
|
|
build_push batchcooking-llm-worker services/tech-step-llm-worker/Dockerfile
|
|
|
|
# Builds the same signed APK as ci.yml's build-android job (issue #64),
|
|
# with one difference: the version baked into the APK is derived from the
|
|
# tag itself rather than left at build.gradle's default. Duplicated here
|
|
# rather than factored into a shared `workflow_call` reusable workflow —
|
|
# deliberate: reusable-workflow support on this self-hosted Forgejo/act
|
|
# runner is unverified, and this ~15-line recipe is proven working as-is
|
|
# in ci.yml; not worth risking a new class of runner-compatibility surprise
|
|
# (this repo has already hit several — see ci-forgejo-runner-constraints
|
|
# in project memory) to save a little duplication. Keep both copies in
|
|
# sync if the Android build steps ever change.
|
|
build-android-apk:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: https://github.com/actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
|
|
- uses: https://github.com/actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: pnpm
|
|
|
|
- uses: https://github.com/actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: "21"
|
|
|
|
- uses: https://github.com/android-actions/setup-android@v3
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Build le bundle web pour Capacitor
|
|
env:
|
|
VITE_API_URL: ${{ secrets.MOBILE_API_URL }}
|
|
run: pnpm --filter web build:capacitor
|
|
- run: pnpm --filter web exec cap sync android
|
|
|
|
- name: Décode le keystore de release
|
|
env:
|
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
run: echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > apps/web/android/app/release.keystore
|
|
|
|
- name: Dérive versionName/versionCode depuis le tag
|
|
# GITHUB_REF_NAME = "v1.4.2" pour un tag `v1.4.2` — versionName =
|
|
# "1.4.2" (sans le `v`), versionCode = majeur*10000 + mineur*100 +
|
|
# correctif (ex. 1.4.2 -> 10402), un entier strictement croissant à
|
|
# chaque release comme Android l'exige (jamais réutilisé, jamais
|
|
# décroissant) tant que chaque partie reste sous 100 (largement
|
|
# suffisant ici). Exposé aux steps suivants via $GITHUB_ENV plutôt
|
|
# que recalculé.
|
|
run: |
|
|
tag="${GITHUB_REF_NAME#v}"
|
|
major="$(echo "$tag" | cut -d. -f1)"
|
|
minor="$(echo "$tag" | cut -d. -f2)"
|
|
patch="$(echo "$tag" | cut -d. -f3)"
|
|
# Préfixe 10# : force une lecture en base 10, sinon bash lirait
|
|
# un composant à zéro non significatif (ex. "08") comme de
|
|
# l'octal invalide et échouerait.
|
|
code=$((10#$major * 10000 + 10#$minor * 100 + 10#$patch))
|
|
echo "Version dérivée du tag $GITHUB_REF_NAME : versionName=$tag versionCode=$code"
|
|
echo "ANDROID_VERSION_NAME=$tag" >> "$GITHUB_ENV"
|
|
echo "ANDROID_VERSION_CODE=$code" >> "$GITHUB_ENV"
|
|
|
|
- name: Build l'APK signé
|
|
working-directory: apps/web/android
|
|
env:
|
|
ANDROID_KEYSTORE_PATH: release.keystore
|
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
run: |
|
|
chmod +x ./gradlew
|
|
./gradlew assembleRelease -PandroidVersionName="$ANDROID_VERSION_NAME" -PandroidVersionCode="$ANDROID_VERSION_CODE"
|
|
|
|
# Nom de fichier stable, jamais versionné : la page d'accueil (#65)
|
|
# pointe vers /releases/latest (Forgejo n'a pas l'équivalent du
|
|
# raccourci GitHub /releases/latest/download/<fichier>, seul
|
|
# /releases/latest lui-même résout) — un nom stable évite au moins que
|
|
# l'utilisateur voie un nom de fichier différent à chaque release une
|
|
# fois sur la page.
|
|
- name: Renomme l'APK sous un nom stable
|
|
run: cp apps/web/android/app/build/outputs/apk/release/app-release.apk batchcooking.apk
|
|
|
|
# v4+ exige l'API "Results" de GitHub, non supportée par ce Forgejo
|
|
# self-hosted ("GHESNotSupportedError") — v3 (ancienne API Artifacts)
|
|
# est la seule utilisable ici, voir ci.yml's build-android pour la
|
|
# même contrainte déjà rencontrée.
|
|
- uses: https://github.com/actions/upload-artifact@v3
|
|
with:
|
|
name: batchcooking.apk
|
|
path: batchcooking.apk
|
|
|
|
forgejo-release:
|
|
runs-on: ubuntu-latest
|
|
needs: [build-and-push-images, build-android-apk]
|
|
steps:
|
|
# Ce runner ne fournit ni docker ni curl par défaut dans ses
|
|
# conteneurs de job — même constat déjà fait pour docker (voir
|
|
# build-and-push-images) et pour curl dans ci.yml's test job.
|
|
- name: Install curl
|
|
run: apt-get update && apt-get install -y curl
|
|
|
|
- uses: https://github.com/actions/checkout@v4
|
|
|
|
- uses: https://github.com/actions/download-artifact@v3
|
|
with:
|
|
name: batchcooking.apk
|
|
|
|
# softprops/action-gh-release (l'action GitHub habituelle pour ça)
|
|
# échoue systématiquement contre ce Forgejo : sa requête de création
|
|
# de release répond 405 (vérifié en conditions réelles, tag v0.2.0 —
|
|
# pas un souci de ref bare comme les autres actions de ce fichier,
|
|
# une vraie incompatibilité d'API entre les deux plateformes qui n'a
|
|
# pas de contournement connu côté configuration de l'action). Appelle
|
|
# directement l'API native de Forgejo plutôt qu'une action tierce
|
|
# dont la compatibilité GitHub-Forgejo n'est pas garantie.
|
|
#
|
|
# Pas d'équivalent à `generate_release_notes` ici (fonctionnalité
|
|
# propre à GitHub, pas répliquée par l'API Forgejo) — le corps de la
|
|
# release reste minimal, cohérent avec l'absence d'outillage de
|
|
# changelog ailleurs dans le repo.
|
|
- name: Crée (ou récupère) la Release Forgejo
|
|
id: create-release
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases"
|
|
body="$(printf '{"tag_name":"%s","name":"%s","draft":false,"prerelease":false}' "$GITHUB_REF_NAME" "$GITHUB_REF_NAME")"
|
|
|
|
# POST pour créer la release, code HTTP capturé à part. Si le tag est
|
|
# déjà connu de Forgejo (tag poussé à la main avant ce workflow, ou
|
|
# ré-exécution du job), l'API répond 409 : on réutilise alors la
|
|
# release existante au lieu d'échouer — idempotence, cf. l'incident
|
|
# du tag v0.2.2 poussé en avance qui a fait tomber ce job sur un 409.
|
|
code="$(curl -sS -o /tmp/release.json -w '%{http_code}' -X POST "$api" \
|
|
-H "Authorization: token $FORGEJO_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$body")"
|
|
|
|
if [ "$code" = "409" ]; then
|
|
echo "Release $GITHUB_REF_NAME déjà existante (HTTP 409) — récupération de son id."
|
|
curl -fsS "$api/tags/$GITHUB_REF_NAME" \
|
|
-H "Authorization: token $FORGEJO_TOKEN" -o /tmp/release.json
|
|
elif [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then
|
|
echo "Échec de création de la Release (HTTP $code) : $(cat /tmp/release.json)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
id="$(grep -o '"id":[0-9]*' /tmp/release.json | head -1 | cut -d: -f2)"
|
|
if [ -z "$id" ]; then
|
|
echo "Pas d'id de release trouvé : $(cat /tmp/release.json)" >&2
|
|
exit 1
|
|
fi
|
|
echo "Release $GITHUB_REF_NAME prête (id=$id)"
|
|
echo "release-id=$id" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Attache l'APK à la Release
|
|
env:
|
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/${{ steps.create-release.outputs.release-id }}/assets"
|
|
curl -fsS -X POST "$api?name=batchcooking.apk" \
|
|
-H "Authorization: token $FORGEJO_TOKEN" \
|
|
-F "attachment=@batchcooking.apk"
|
|
|
|
notify-portainer:
|
|
runs-on: ubuntu-latest
|
|
needs: forgejo-release
|
|
steps:
|
|
- name: Trigger Portainer redeploy
|
|
# Best-effort: does nothing (and doesn't fail the workflow) until
|
|
# the PORTAINER_WEBHOOK_URL repo secret is set — grab that URL from
|
|
# the stack's webhook setting in Portainer and add it as a secret
|
|
# named PORTAINER_WEBHOOK_URL (see README).
|
|
env:
|
|
PORTAINER_WEBHOOK_URL: ${{ secrets.PORTAINER_WEBHOOK_URL }}
|
|
run: |
|
|
if [ -z "$PORTAINER_WEBHOOK_URL" ]; then
|
|
echo "PORTAINER_WEBHOOK_URL not set — skipping, Portainer will pick this up on its next poll."
|
|
exit 0
|
|
fi
|
|
curl -fsS -X POST "$PORTAINER_WEBHOOK_URL"
|