#14: Gitea CI, registry composes, Node 22 image and changelog

- Gitea Actions: Trivy, Vitest in a Dockerfile test stage, arm64 image build, image
  scan and smoke test; releases reuse the image by content hash.
- docker-compose.<edition>.registry.yaml run the stack from the published image.
- Dockerfile: Node 22, a test stage, and no npm in the runtime image.
- README, CLAUDE.md and CHANGELOG describe the CI, the image and the release flow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-01 22:54:00 -03:00
1 parent 640e52c0c9
commit a4a2584f37
12 files changed
+606 -9

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Uso: scripts/plano-imagem.sh <registro> <repositório>
# Imprime inputs=, image= e reuse= (para $GITHUB_OUTPUT). O hash cobre só os
# arquivos que entram no build da imagem, não a documentação nem a configuração
# de ferramentas. reuse=true quando o registro já tem inputs-<hash>.
set -euo pipefail
registry=$1; ns=${2,,}
inputs=$(git ls-tree -r HEAD \
| awk -F'\t' '$2 !~ /^(\.gitea|\.github|\.claude|\.superpowers|\.wolf)\// && $2 !~ /^(README.*|CHANGELOG\.md|CLAUDE\.md|LICENSE|\.gitignore|\.gitattributes|\.trivyignore|docker-compose.*\.yaml|\.env\.example|scripts\/release-notes\.sh)$/' \
| sha256sum | cut -c1-16)
token=$(curl -fsS "https://$registry/v2/token?service=container_registry&scope=repository:$ns:pull" | jq -r .token)
code=$(curl -s -o /dev/null -w '%{http_code}' -I \
-H "Authorization: Bearer $token" \
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json' \
"https://$registry/v2/$ns/manifests/inputs-$inputs")
echo "inputs-$inputs -> HTTP $code" >&2
reuse=false; [ "$code" = 200 ] && reuse=true
echo "inputs=$inputs"
echo "image=$registry/$ns"
echo "reuse=$reuse"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Imprime as notas de uma release: a imagem publicada (quando houver) e a seção
# do CHANGELOG.md daquela versão (ou o log de commits desde a tag anterior, se
# não houver seção).
#
# scripts/release-notes.sh v1.2.3 [git.exemplo.com/dono/projeto]
set -euo pipefail
tag=${1:?uso: release-notes.sh <tag> [imagem]}
image=${2:-}
version=${tag#v}
if [ -n "$image" ]; then
echo "## Imagem"
echo
echo '```'
echo "$image:$version"
echo '```'
echo
fi
section=""
if [ -f CHANGELOG.md ]; then
section=$(awk -v head="## [$version]" '
index($0, head) == 1 { found = 1; next }
found && /^## \[/ { exit }
found && /^\[[^]]+\]: / { next }
found { print }
' CHANGELOG.md)
fi
if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
printf '%s\n' "$section"
else
previous=$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)
echo "## Mudanças${previous:+ desde $previous}"
echo
git log --no-merges --format='- %s (%h)' "${previous:+$previous..}$tag"
fi
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Teste de fumaça da imagem do CraftDock, sem o servidor de Minecraft: o painel sobe,
# serve a interface, recusa quem não está logado, traz os ícones gerados no build e o
# healthcheck da própria imagem fica saudável.
#
# scripts/smoke-imagem.sh <imagem>
#
# Cada falha é impressa e o script sai com 1 no fim, para mostrar tudo de uma vez.
set -uo pipefail
imagem=${1:?uso: smoke-imagem.sh <imagem>}
app=craftdock-smoke-${GITHUB_RUN_ID:-$$}
falhas=0
falhou() { echo "FALHOU $1"; falhas=$((falhas + 1)); }
# Os daemons dos runners persistem entre jobs: limpa sempre, inclusive se falhar.
trap 'docker rm -f "$app" >/dev/null 2>&1' EXIT
# Valores descartáveis. O hash é o de teste que a suíte já usa (config.test.js); a
# senha dele não importa aqui, o smoke não faz login. Sem docker.sock: o painel sobe e
# só as rotas que falam com o Docker deixam de funcionar.
hash='$2a$12$YjrtHh2BNcUT0P76L2ZUF.mCtxzUwN.gZ4siUqji2oxrDbIXRdvfe'
docker run -d --name "$app" \
-e ADMIN_PASSWORD_HASH_B64="$(printf %s "$hash" | base64 -w0)" \
-e SESSION_SECRET=segredo-do-smoke \
"$imagem" >/dev/null || exit 1
ip=""
for _ in $(seq 1 40); do
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$app" 2>/dev/null)
[ -n "$ip" ] && curl -fsS -o /dev/null "http://$ip:3000/api/health" 2>/dev/null && break
sleep 1
done
base=http://$ip:3000
curl -fsS -o /dev/null "$base/api/health" || { echo "FALHOU /api/health: sem resposta"; docker logs "$app" | tail -20; exit 1; }
status() { [ "$(curl -s -o /dev/null -w '%{http_code}' "$base$2")" = "$1" ] || falhou "$2: esperado $1"; }
status 200 /
status 200 /api/health
# Tudo sob /api, menos auth e health, exige sessão.
status 401 /api/status
status 401 /api/players
status 401 /api/properties
status 401 /api/world
curl -s "$base/" | grep -qi 'craftdock' || falhou "/: a interface não carregou"
curl -s "$base/api/health" | grep -q '"status":"ok"' || falhou "/api/health: corpo inesperado"
# Os ícones de itens são gerados no build (não ficam no repositório).
n=$(docker exec "$app" sh -c 'ls /app/src/public/assets/mc 2>/dev/null | wc -l')
[ "${n:-0}" -gt 0 ] || falhou "os ícones do Minecraft não foram gerados no build"
# O addon nativo do LevelDB (leitura de mundos Bedrock) carrega na imagem final.
docker exec "$app" node -e "import('leveldb-zlib').then(()=>process.exit(0)).catch(e=>{console.error(e.message);process.exit(1)})" || falhou "leveldb-zlib não carrega"
# O healthcheck da própria imagem (src/healthcheck.js) reporta saudável.
for _ in $(seq 1 30); do
estado=$(docker inspect -f '{{.State.Health.Status}}' "$app" 2>/dev/null)
[ "$estado" = healthy ] && break
sleep 2
done
[ "$estado" = healthy ] || falhou "o healthcheck da imagem não ficou healthy (estado: ${estado:-nenhum})"
[ "$falhas" = 0 ] && echo "ok: interface, autenticação, ícones, LevelDB e healthcheck conferidos"
[ "$falhas" = 0 ]