All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 14m48s
Ik ging ervan uit dat de bereikbaarheidscontrole niet kon slagen omdat de runner in een eigen container zit. De eerste run liet zien dat hij wél slaagt: "Antwoordt op http://127.0.0.1:5000". De exitcode wordt nu meegenomen, dus een echte fout in de deploy laat de CI vallen in plaats van alleen in het log te belanden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
274 lines
No EOL
11 KiB
YAML
274 lines
No EOL
11 KiB
YAML
name: Deploy server-up (dev)
|
|
|
|
on:
|
|
push:
|
|
branches: [dev]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: server-up-deploy-dev
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: [self-hosted, dev]
|
|
timeout-minutes: 20
|
|
|
|
env:
|
|
DEPLOY_DIR: /opt/docker/server-up
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Bepaal versie
|
|
id: version
|
|
run: |
|
|
set -euo pipefail
|
|
BASE="$(tr -d '[:space:]' < VERSION 2>/dev/null || echo 0.0.0)"
|
|
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
CHANNEL="release"
|
|
else
|
|
VERSION="${BASE}"
|
|
CHANNEL="dev"
|
|
fi
|
|
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
echo "CHANNEL=${CHANNEL}" >> "$GITHUB_ENV"
|
|
# SU_VERSION wordt door docker compose gebruikt als build-arg
|
|
echo "SU_VERSION=${VERSION}" >> "$GITHUB_ENV"
|
|
echo "Deploying version: ${VERSION} (${CHANNEL})"
|
|
|
|
- name: Tests draaien
|
|
run: |
|
|
set -euo pipefail
|
|
# In een container in plaats van een venv op de runner: die heeft
|
|
# geen python3-venv, en zo draaien de tests bovendien op exact de
|
|
# Python-versie waarop de app in productie draait.
|
|
# PYTHONDONTWRITEBYTECODE + no:cacheprovider voorkomen dat er
|
|
# root-eigen bestanden in de werkmap achterblijven.
|
|
docker run --rm \
|
|
-v "$PWD:/w" -w /w \
|
|
-v su-pip-cache:/root/.cache/pip \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
python:3.12-slim \
|
|
sh -c "pip install -q -r server-up/requirements.txt pytest \
|
|
&& python -m pytest tests -q -p no:cacheprovider"
|
|
|
|
- name: Compose-bestanden valideren
|
|
run: |
|
|
set -euo pipefail
|
|
# De testsuite controleert of de sjablonen geldige YAML opleveren.
|
|
# Of `docker compose` ze ook accepteert is een andere vraag: een
|
|
# depends_on in de verkeerde vorm of network_mode naast networks is
|
|
# prima YAML en een kapotte stack. Dat merk je anders pas als iemand
|
|
# de app installeert.
|
|
#
|
|
# Renderen kan alleen waar Python staat (in de container), valideren
|
|
# alleen waar Docker staat (op de runner). Vandaar twee helften.
|
|
docker run --rm \
|
|
-v "$PWD:/w" -w /w \
|
|
-v su-pip-cache:/root/.cache/pip \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
python:3.12-slim \
|
|
sh -c "pip install -q -r server-up/requirements.txt \
|
|
&& python tools/render_voor_validatie.py .compose-check \
|
|
&& chown -R $(id -u):$(id -g) .compose-check"
|
|
fouten=0
|
|
for f in .compose-check/*/docker-compose.yml; do
|
|
if ! uitvoer=$(docker compose -f "$f" config -q 2>&1); then
|
|
echo "✖ $(basename "$(dirname "$f")")"
|
|
echo "$uitvoer" | sed 's/^/ /'
|
|
fouten=$((fouten + 1))
|
|
fi
|
|
done
|
|
rm -rf .compose-check
|
|
if [ "$fouten" -ne 0 ]; then
|
|
echo "$fouten sjablo(o)n(en) leveren een compose-bestand op dat docker weigert"
|
|
exit 1
|
|
fi
|
|
echo "Alle compose-bestanden zijn geldig"
|
|
|
|
- name: Images controleren
|
|
run: |
|
|
set -euo pipefail
|
|
# Een image dat niet meer bestaat merk je anders pas als iemand op
|
|
# installeren drukt. Zo verdween Huntarr nadat dat project offline
|
|
# ging, en wezen Forgejo, Planka en Baby Buddy naar tags die er niet
|
|
# zijn. Draait bewust rustig: registries knijpen af, en een afgeknepen
|
|
# verzoek is niet te onderscheiden van een ontbrekend image.
|
|
docker run --rm \
|
|
-v "$PWD:/w" -w /w \
|
|
-e PYTHONDONTWRITEBYTECODE=1 \
|
|
python:3.12-slim \
|
|
python tools/controleer_images.py
|
|
|
|
- name: Sync code naar deploy-directory
|
|
run: |
|
|
set -euo pipefail
|
|
# .env staat in .gitignore, dus de checkout heeft er geen. Zonder deze
|
|
# uitzondering wist --delete de lokale instellingen van de server
|
|
# (BIND, SU_IMAGE, PORT) bij elke deploy.
|
|
rsync -a --no-group --delete \
|
|
--exclude='.git/' \
|
|
--exclude='.forgejo/' \
|
|
--exclude='.env' \
|
|
./ "$DEPLOY_DIR/"
|
|
|
|
- name: Instellingen en versie klaarzetten
|
|
run: |
|
|
set -euo pipefail
|
|
# Expliciet cd in plaats van 'working-directory:'. Dat laatste wordt
|
|
# niet door elke runner-implementatie even betrouwbaar toegepast, en
|
|
# als het misgaat belanden VERSION, .env en de container stilletjes in
|
|
# de werkmap van de runner in plaats van in de deploy-map.
|
|
cd "$DEPLOY_DIR"
|
|
echo "Werkmap: $(pwd)"
|
|
|
|
echo "${VERSION}" > VERSION
|
|
|
|
# SU_TAG bepaalt welk image compose start. We schrijven dat bij
|
|
# voorkeur in .env zodat het een handmatige `docker compose up`
|
|
# overleeft, maar dat bestand kan van een andere gebruiker zijn (bv.
|
|
# met sudo aangemaakt). Lukt schrijven niet, dan geven we SU_TAG mee
|
|
# via de omgeving: compose gebruikt die en de deploy loopt gewoon door.
|
|
if [ ! -f .env ] && [ -f .env.example ]; then
|
|
cp .env.example .env 2>/dev/null \
|
|
&& echo "Nieuwe .env aangemaakt vanuit .env.example" \
|
|
|| echo "LET OP: kon geen .env aanmaken in $(pwd)"
|
|
fi
|
|
|
|
if [ -w .env ] || { [ ! -e .env ] && [ -w . ]; }; then
|
|
touch .env
|
|
sed -i '/^SU_TAG=/d' .env
|
|
echo "SU_TAG=${VERSION}" >> .env
|
|
echo "Inhoud van $(pwd)/.env:"
|
|
grep -vE '^\s*(#|$)' .env | sed 's/^/ /'
|
|
else
|
|
echo "LET OP: $(pwd)/.env is niet schrijfbaar voor $(id -un)."
|
|
echo " Eigenaar: $(stat -c '%U:%G %a' .env 2>/dev/null || echo onbekend)"
|
|
echo " Herstel eenmalig met:"
|
|
echo " sudo chown $(stat -c '%U:%G' . ) $(pwd)/.env"
|
|
echo " SU_TAG wordt nu via de omgeving meegegeven."
|
|
fi
|
|
|
|
- name: Build image
|
|
run: |
|
|
set -euo pipefail
|
|
cd "$DEPLOY_DIR"
|
|
export SU_TAG="${VERSION}"
|
|
docker compose build --pull
|
|
docker tag "server-up:${VERSION}" "server-up:${CHANNEL}" || true
|
|
|
|
# Server Up draait niet meer als root. Dat hangt aan het entrypoint en aan
|
|
# `setpriv`, en beide zijn alleen in een echte container te toetsen — de
|
|
# pytest-suite kan er niet bij. Zonder deze stap merk je een regressie hier
|
|
# pas als iemands installatie niet meer opkomt.
|
|
- name: Niet-root modus controleren
|
|
run: |
|
|
set -euo pipefail
|
|
IMG="server-up:${VERSION}"
|
|
|
|
echo "1. setpriv aanwezig in het image"
|
|
docker run --rm --entrypoint setpriv "$IMG" --help >/dev/null
|
|
echo " ok"
|
|
|
|
echo "2. zakt af naar de opgegeven gebruiker"
|
|
uit=$(docker run --rm -e SU_UID=1000 -e SU_GID=1000 \
|
|
-v /var/run/docker.sock:/var/run/docker.sock "$IMG" id)
|
|
echo " $uit"
|
|
case "$uit" in
|
|
uid=1000*) ;;
|
|
*) echo "FOUT: draait niet als uid 1000"; exit 1 ;;
|
|
esac
|
|
|
|
echo "3. zit in de groep van de docker-socket"
|
|
sok_gid=$(stat -c %g /var/run/docker.sock)
|
|
case "$uit" in
|
|
*"$sok_gid"*) echo " gid $sok_gid aanwezig" ;;
|
|
*) echo "FOUT: gid $sok_gid van de socket ontbreekt"; exit 1 ;;
|
|
esac
|
|
|
|
echo "4. kan schrijven in /data"
|
|
docker run --rm -e SU_UID=1000 -e SU_GID=1000 \
|
|
-v "su-nietroot-proef-${VERSION}:/data" "$IMG" \
|
|
sh -c 'touch /data/proef && echo " ok"'
|
|
docker volume rm "su-nietroot-proef-${VERSION}" >/dev/null
|
|
|
|
echo "5. zonder SU_UID blijft het root — de ontsnappingsklep"
|
|
uit0=$(docker run --rm "$IMG" id)
|
|
echo " $uit0"
|
|
case "$uit0" in
|
|
uid=0*) ;;
|
|
*) echo "FOUT: SU_UID=0 hoort root te blijven"; exit 1 ;;
|
|
esac
|
|
|
|
- name: Deploy
|
|
run: |
|
|
set -euo pipefail
|
|
cd "$DEPLOY_DIR"
|
|
export SU_TAG="${VERSION}"
|
|
docker compose up -d --remove-orphans
|
|
docker compose ps
|
|
# Waar draait de container vandaan, en op welk adres luistert hij?
|
|
echo "Compose-projectmap volgens de container:"
|
|
docker inspect server-up \
|
|
--format ' {{index .Config.Labels "com.docker.compose.project.working_dir"}}'
|
|
echo "Poortbinding:"
|
|
docker inspect server-up \
|
|
--format ' {{range $p, $c := .NetworkSettings.Ports}}{{$p}} -> {{range $c}}{{.HostIp}}:{{.HostPort}}{{end}}{{"\n"}}{{end}}'
|
|
|
|
- name: Wachten op healthcheck
|
|
run: |
|
|
set -euo pipefail
|
|
for i in {1..40}; do
|
|
status=$(docker inspect --format '{{.State.Health.Status}}' server-up 2>/dev/null || echo "starting")
|
|
echo "poll $i: $status"
|
|
if [ "$status" = "healthy" ]; then
|
|
echo "server-up is healthy als versie ${VERSION}"
|
|
exit 0
|
|
fi
|
|
sleep 3
|
|
done
|
|
echo "server-up werd niet healthy in tijd — logs:"
|
|
cd "$DEPLOY_DIR" && docker compose logs --tail=200
|
|
exit 1
|
|
|
|
# `--doctor` is ruim honderd regels shell die alleen tegen een échte
|
|
# installatie draaien. De pytest-suite komt niet verder dan "er is geen
|
|
# Docker"; hier wel.
|
|
- name: install.sh --doctor doorlichten
|
|
if: success()
|
|
run: |
|
|
set -uo pipefail
|
|
uit="$(sh "$DEPLOY_DIR/install.sh" --doctor --dir "$DEPLOY_DIR" 2>&1)"
|
|
code=$?
|
|
printf '%s\n' "$uit"
|
|
|
|
# Alle secties moeten gedraaid hebben: een shell-fout halverwege zou
|
|
# anders als "niets aan de hand" langskomen.
|
|
for kop in "Installatie" "Docker" "Container" "Bereikbaarheid" \
|
|
"Account" "Mappen" "Bijwerken vanuit de interface"; do
|
|
printf '%s\n' "$uit" | grep -q "^$kop$" || {
|
|
echo "FOUT: --doctor kwam niet tot de sectie '$kop'"; exit 1; }
|
|
done
|
|
|
|
# En de uitslag telt mee. Waarschuwingen mogen (deze deploy draait
|
|
# bewust nog als root), fouten niet.
|
|
if [ "$code" -ne 0 ]; then
|
|
echo "FOUT: --doctor meldt problemen met de deploy"
|
|
exit 1
|
|
fi
|
|
echo "--doctor liep van begin tot eind, zonder fouten"
|
|
|
|
- name: Oude images opruimen (behoud laatste 5)
|
|
if: success()
|
|
run: |
|
|
docker images server-up --format '{{.Tag}} {{.ID}}' \
|
|
| grep -vE '^(latest|release|dev) ' \
|
|
| tail -n +6 \
|
|
| awk '{print $2}' \
|
|
| xargs -r docker rmi -f || true
|
|
docker image prune -f |