Merge feature/14-gitea-import
Release / Plano (reaproveitar a imagem?) (push) Successful in 4s
CI / Trivy (dependências, segredos e configuração) (push) Failing after 6s
Release / ci (push) Failing after 7s
CI / Testes e imagem Docker (linux/arm64) (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Release / Plano (reaproveitar a imagem?) (push) Successful in 4s
CI / Trivy (dependências, segredos e configuração) (push) Failing after 6s
Release / ci (push) Failing after 7s
CI / Testes e imagem Docker (linux/arm64) (push) Skipped
Release / Publica a imagem (linux/arm64) (push) Skipped
Release / Cria a release no Gitea (push) Skipped
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
a735cb0da8
12 files changed
+606
-9
No files matched your search
+5
-1
@@ -1,7 +1,11 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.git
|
.git
|
||||||
|
.gitea
|
||||||
|
.gitattributes
|
||||||
.env
|
.env
|
||||||
test
|
.env.*
|
||||||
|
!.env.example
|
||||||
*.md
|
*.md
|
||||||
coverage
|
coverage
|
||||||
src/public/assets/mc
|
src/public/assets/mc
|
||||||
|
.superpowers
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# Documentação e configuração de ferramenta não mudam nada que a suíte teste.
|
||||||
|
# Na main não roda por push: o release.yml é quem a chama, e só quando a imagem
|
||||||
|
# precisa ser reconstruída; rodar nos dois repetiria a mesma suíte no mesmo commit.
|
||||||
|
# Sem gatilho de pull_request: o push da branch já valida o mesmo commit, e os
|
||||||
|
# dois juntos rodariam tudo duas vezes (o repositório não recebe PR de fork).
|
||||||
|
#
|
||||||
|
# A imagem é construída aqui uma única vez, em arm64 (a arquitetura do servidor),
|
||||||
|
# testada e publicada como inputs-<hash>. Ao fazer o merge, o release encontra
|
||||||
|
# essa tag e só aplica os nomes novos, sem rodar nem construir nada de novo.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore: [main]
|
||||||
|
paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.superpowers/**', '.wolf/**', 'docker-compose*.yaml', '.env.example', 'scripts/release-notes.sh']
|
||||||
|
schedule:
|
||||||
|
- cron: '0 9 * * 1' # toda segunda: reavalia com a base de vulnerabilidades nova
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Trivy roda pela imagem oficial: o trivy-action clona o repositório do Trivy
|
||||||
|
# do mesmo servidor do workflow, que aqui é o Gitea. O workspace do job não é
|
||||||
|
# visível ao daemon, então os arquivos entram no container com `docker cp`.
|
||||||
|
# O .trivyignore do repositório (DS-0002, documentado) é lido automaticamente.
|
||||||
|
seguranca:
|
||||||
|
name: Trivy (dependências, segredos e configuração)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Repositório
|
||||||
|
run: |
|
||||||
|
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
|
||||||
|
fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 .)
|
||||||
|
docker cp . "$cid:/src"
|
||||||
|
docker start -a "$cid"
|
||||||
|
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
exit "$status"
|
||||||
|
|
||||||
|
imagem:
|
||||||
|
name: Testes e imagem Docker (linux/arm64)
|
||||||
|
needs: [seguranca]
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 40
|
||||||
|
env:
|
||||||
|
REGISTRY: git.lucasrguerra.dev.br
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Conteúdo já publicado (re-push, tag num commit já validado): nada a fazer.
|
||||||
|
- id: plano
|
||||||
|
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Vitest dentro do estágio `test` do Dockerfile: ele compila o addon nativo do
|
||||||
|
# LevelDB uma vez, e as camadas ficam no daemon do runner para o build abaixo.
|
||||||
|
- name: Testes (Vitest)
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: docker build --target test -t craftdock:test .
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: docker build -t craftdock:ci .
|
||||||
|
|
||||||
|
- name: Trivy na imagem
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: |
|
||||||
|
docker run --rm --network host -v trivy-cache:/root/.cache/ -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \
|
||||||
|
image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 craftdock:ci
|
||||||
|
|
||||||
|
- name: A imagem sobe e responde
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: scripts/smoke-imagem.sh craftdock:ci
|
||||||
|
|
||||||
|
# O login fica por último: testes, build, Trivy e smoke não precisam de credencial.
|
||||||
|
- if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
# Passou em tudo: publica a mesma imagem que foi testada, sem reconstruir.
|
||||||
|
- name: Publica inputs-<hash>
|
||||||
|
if: ${{ steps.plano.outputs.reuse != 'true' }}
|
||||||
|
run: |
|
||||||
|
tag="${{ steps.plano.outputs.image }}:inputs-${{ steps.plano.outputs.inputs }}"
|
||||||
|
docker tag craftdock:ci "$tag"
|
||||||
|
docker push "$tag"
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Publica a imagem no registro de containers do Gitea depois de um merge em main,
|
||||||
|
# e de novo com tag semver quando uma tag v* é enviada. O servidor só faz pull.
|
||||||
|
# Precisa do secret REGISTRY_TOKEN no repositório (token do Gitea com write:package):
|
||||||
|
# o registro recusa o token do job.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.lucasrguerra.dev.br
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# A imagem só depende dos arquivos que entram no build, não da documentação nem
|
||||||
|
# da configuração de ferramentas. O push da branch já a construiu, testou e
|
||||||
|
# publicou como inputs-<hash>; quando essa tag existe (o caso normal de um
|
||||||
|
# merge, uma tag num commit validado, um merge só de docs), não há o que
|
||||||
|
# construir: as tags novas apontam para o manifesto existente.
|
||||||
|
plano:
|
||||||
|
name: Plano (reaproveitar a imagem?)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
inputs: ${{ steps.plano.outputs.inputs }}
|
||||||
|
reuse: ${{ steps.plano.outputs.reuse }}
|
||||||
|
image: ${{ steps.plano.outputs.image }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- id: plano
|
||||||
|
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Nada é publicado sem a suíte completa passar antes. Pulada quando a imagem é
|
||||||
|
# reaproveitada: o mesmo conteúdo já passou por ela.
|
||||||
|
ci:
|
||||||
|
needs: plano
|
||||||
|
if: ${{ needs.plano.outputs.reuse != 'true' }}
|
||||||
|
uses: ./.gitea/workflows/ci.yml
|
||||||
|
# Workflows reutilizáveis não herdam os secrets sozinhos: o login no registro precisa deles.
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
publicar:
|
||||||
|
name: Publica a imagem (linux/arm64)
|
||||||
|
needs: [plano, ci]
|
||||||
|
# ci é pulado, e não bem-sucedido, quando a imagem é reaproveitada.
|
||||||
|
if: ${{ !cancelled() && needs.plano.result == 'success' && (needs.ci.result == 'success' || needs.plano.outputs.reuse == 'true') }}
|
||||||
|
# O único runner é arm64 e o servidor de destino (Oracle) também, então a
|
||||||
|
# imagem é construída nativamente, sem emulação.
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Se o ci chamado acima acabou de publicar a imagem (push direto na main),
|
||||||
|
# o plano lá de cima está desatualizado: confere de novo antes de construir.
|
||||||
|
- id: replano
|
||||||
|
run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Autentica no registro
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Metadados
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: ${{ needs.plano.outputs.image }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest
|
||||||
|
type=sha,format=long
|
||||||
|
|
||||||
|
- name: Build e push
|
||||||
|
if: ${{ steps.replano.outputs.reuse != 'true' }}
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=${{ needs.plano.outputs.image }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ needs.plano.outputs.image }}:buildcache,mode=max
|
||||||
|
|
||||||
|
# Mesmo manifesto, nomes novos: nenhuma camada é lida nem escrita.
|
||||||
|
- name: Aplica as tags à imagem existente
|
||||||
|
if: ${{ steps.replano.outputs.reuse == 'true' }}
|
||||||
|
run: |
|
||||||
|
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
||||||
|
docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}"
|
||||||
|
|
||||||
|
# Cria a entrada em "Releases" quando a execução é de uma tag v*. Roda depois
|
||||||
|
# de publicar a imagem, com as notas da seção do CHANGELOG.md.
|
||||||
|
release:
|
||||||
|
name: Cria a release no Gitea
|
||||||
|
needs: [plano, publicar]
|
||||||
|
# Explícito, como o publicar: o ci acima é pulado quando a imagem é
|
||||||
|
# reaproveitada, e o success() implícito pularia este job também.
|
||||||
|
if: ${{ !cancelled() && needs.publicar.result == 'success' && startsWith(gitea.ref, 'refs/tags/v') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
IMAGE: ${{ needs.plano.outputs.image }}
|
||||||
|
run: |
|
||||||
|
scripts/release-notes.sh "$TAG" "$IMAGE" > notes.md
|
||||||
|
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
||||||
|
jq -n --arg tag "$TAG" --arg name "CraftDock ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
||||||
|
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||||
|
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' -d @release.json
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Everything notable in CraftDock is recorded here. The format follows
|
||||||
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows
|
||||||
|
[Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-11-01
|
||||||
|
|
||||||
|
First versioned release. It collects the state of CraftDock on 2026-11-01.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Web panel to manage a single Minecraft server (Java or Bedrock) running as a Docker
|
||||||
|
container: start / stop / restart / kill, live CPU, RAM and player count, real-time
|
||||||
|
console over WebSocket, `server.properties` editor, world download / upload / regeneration,
|
||||||
|
seed and seed map, and a persistent player directory with a per-player inspector
|
||||||
|
(position, health, inventory, XP) read from the world save (Java `playerdata`, Bedrock
|
||||||
|
LevelDB).
|
||||||
|
- Adapter layer for the two editions (Java over RCON, Bedrock over `docker attach`).
|
||||||
|
- Structured JSON logging, a bcrypt-authenticated session on every route and socket,
|
||||||
|
and a self-healing healthcheck.
|
||||||
|
- `docker-compose.<edition>.registry.yaml`, which runs the stack from the published image.
|
||||||
|
- A CI on the Gitea runners (`.gitea/workflows`): Trivy on the repository and on the image,
|
||||||
|
the Vitest suite, an arm64 image build and a smoke test. The image is built once per
|
||||||
|
content hash and reused by the release.
|
||||||
|
- `CHANGELOG.md`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The code, the container image and the releases now live on Gitea
|
||||||
|
(`git.lucasrguerra.dev.br`); the deployment pulls the image from its registry.
|
||||||
|
- Dockerfile: Node 20 → 22; a dedicated `test` stage runs the suite on the same compiled
|
||||||
|
dependencies; the runtime image no longer carries `npm`.
|
||||||
|
- The repository history was lost with the previous GitHub account, so this repository starts
|
||||||
|
from a fresh history at the 2026-11-01 state.
|
||||||
|
- `.gitattributes` forces LF line endings.
|
||||||
|
|
||||||
|
[Unreleased]: https://git.lucasrguerra.dev.br/lucasrguerra/craftdock/compare/v0.1.0...HEAD
|
||||||
|
[0.1.0]: https://git.lucasrguerra.dev.br/lucasrguerra/craftdock/releases/tag/v0.1.0
|
||||||
@@ -15,8 +15,12 @@ npm run dev # dev server (loads .env, watches files), panel on :3000
|
|||||||
npm run hash -- <pw> # generate the admin bcrypt hash (raw + base64 forms)
|
npm run hash -- <pw> # generate the admin bcrypt hash (raw + base64 forms)
|
||||||
docker compose -f docker-compose.bedrock.yaml up -d --build # full Bedrock stack
|
docker compose -f docker-compose.bedrock.yaml up -d --build # full Bedrock stack
|
||||||
docker compose -f docker-compose.java.yaml up -d --build # full Java stack
|
docker compose -f docker-compose.java.yaml up -d --build # full Java stack
|
||||||
|
docker build --target test . # Vitest inside the image (what CI runs)
|
||||||
|
scripts/smoke-imagem.sh <image> # image smoke test (what CI runs)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
CI/CD lives in `.gitea/workflows` (Gitea runners): `ci.yml` on branches, `release.yml` on `main` and `vX.Y.Z` tags. The image is `git.lucasrguerra.dev.br/lucasrguerra/craftdock` (public). Remote: `gitea`. Keep `CHANGELOG.md` updated under *Unreleased* and release with `git tag vX.Y.Z && git push gitea vX.Y.Z`.
|
||||||
|
|
||||||
Run a single test file: `npx vitest run test/services/playerDetailService.test.js`.
|
Run a single test file: `npx vitest run test/services/playerDetailService.test.js`.
|
||||||
|
|
||||||
## Git workflow (mandatory)
|
## Git workflow (mandatory)
|
||||||
|
|||||||
+21
-6
@@ -4,7 +4,9 @@
|
|||||||
# Why not Alpine? `leveldb-zlib` is a native addon that compiles from source via
|
# Why not Alpine? `leveldb-zlib` is a native addon that compiles from source via
|
||||||
# cmake-js. On Alpine/musl (esp. ARM64) that build repeatedly failed at the CMake
|
# cmake-js. On Alpine/musl (esp. ARM64) that build repeatedly failed at the CMake
|
||||||
# configure step; Debian glibc is the reliable path for this toolchain.
|
# configure step; Debian glibc is the reliable path for this toolchain.
|
||||||
FROM node:20-bookworm-slim AS builder
|
|
||||||
|
# ── deps: every dependency (dev included) with the native addon compiled ──
|
||||||
|
FROM node:22-bookworm-slim AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
# scripts/ must exist before npm ci: the postinstall hook runs the (no-op here)
|
# scripts/ must exist before npm ci: the postinstall hook runs the (no-op here)
|
||||||
@@ -13,24 +15,37 @@ COPY package*.json ./
|
|||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends python3 make g++ cmake zlib1g-dev ca-certificates \
|
&& apt-get install -y --no-install-recommends python3 make g++ cmake zlib1g-dev ca-certificates \
|
||||||
&& npm ci --omit=dev
|
&& npm ci
|
||||||
|
|
||||||
|
# ── test: `docker build --target test .` runs the Vitest suite (used by CI) ──
|
||||||
|
FROM deps AS test
|
||||||
|
COPY src ./src
|
||||||
|
COPY test ./test
|
||||||
|
COPY vitest.config.js ./
|
||||||
|
RUN npm test
|
||||||
|
|
||||||
|
# ── builder: production dependencies only ──
|
||||||
|
FROM deps AS builder
|
||||||
|
RUN npm prune --omit=dev
|
||||||
|
|
||||||
# Item icons are generated at build time (not committed, not downloaded at
|
# Item icons are generated at build time (not committed, not downloaded at
|
||||||
# runtime): install the versioned minecraft-assets package and extract the flat
|
# runtime): install the versioned minecraft-assets package and extract the flat
|
||||||
# item PNGs. Bump ASSETS_VERSION (or rebuild) to pick up new game items.
|
# item PNGs. Bump ASSETS_VERSION (or rebuild) to pick up new game items.
|
||||||
FROM node:20-bookworm-slim AS assets
|
FROM node:22-bookworm-slim AS assets
|
||||||
ARG ASSETS_VERSION=latest
|
ARG ASSETS_VERSION=latest
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY scripts/extract-mc-assets.mjs scripts/
|
COPY scripts/extract-mc-assets.mjs scripts/
|
||||||
RUN npm install --no-save --ignore-scripts minecraft-assets@${ASSETS_VERSION} \
|
RUN npm install --no-save --ignore-scripts minecraft-assets@${ASSETS_VERSION} \
|
||||||
&& node scripts/extract-mc-assets.mjs --required
|
&& node scripts/extract-mc-assets.mjs --required
|
||||||
|
|
||||||
FROM node:20-bookworm-slim
|
FROM node:22-bookworm-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# Patch fixable OS CVEs in the base image at build time (trivy image scan).
|
# Patch fixable OS CVEs in the base image at build time (trivy image scan). The
|
||||||
|
# runtime only needs node: the npm bundled in the base image carries its own
|
||||||
|
# dependencies, which Trivy reports and nothing here uses.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
|
||||||
# The compiled node-leveldb.node links libz (zlib1g) and libstdc++6, both already
|
# The compiled node-leveldb.node links libz (zlib1g) and libstdc++6, both already
|
||||||
# present in the slim base image — so no extra runtime packages are needed.
|
# present in the slim base image — so no extra runtime packages are needed.
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ docker compose -f docker-compose.bedrock.yaml logs -f # follow panel + server
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- **Node.js ≥ 18** (dev) or Docker (deploy).
|
- **Node.js ≥ 18** (dev; the image uses Node 22) or Docker (deploy).
|
||||||
- A Docker host — the panel mounts `/var/run/docker.sock` to control the game container.
|
- A Docker host — the panel mounts `/var/run/docker.sock` to control the game container.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -100,7 +100,12 @@ Open <http://localhost:3000> and log in with your password.
|
|||||||
|
|
||||||
## Running with Docker
|
## Running with Docker
|
||||||
|
|
||||||
The panel image is built locally from the included `Dockerfile` — it is **not** published to any registry, so a plain `docker compose pull`/`up` without a build step fails with *"pull access denied"*. Always build it:
|
The panel image is built from the included `Dockerfile`. Two ways to run each stack:
|
||||||
|
|
||||||
|
- **Build it yourself** (`docker-compose.<edition>.yaml`, `--build`): works anywhere, no registry needed.
|
||||||
|
- **Pull the published image** (`docker-compose.<edition>.registry.yaml`): `git.lucasrguerra.dev.br/lucasrguerra/craftdock` (linux/arm64), built, scanned and smoke-tested by CI. Tags: `latest`, `X.Y.Z`, `X.Y` and `sha-<commit>`; pin one with `IMAGE_TAG`. This is what the reference Coolify deployment runs.
|
||||||
|
|
||||||
|
To build it:
|
||||||
|
|
||||||
Two reference stacks are provided:
|
Two reference stacks are provided:
|
||||||
|
|
||||||
@@ -268,3 +273,14 @@ src/
|
|||||||
```
|
```
|
||||||
|
|
||||||
CraftDock also keeps two metadata files at the data root (bundled into world backups): `craftdock_players.json` (persistent XUID→name player directory) and `craftdock_bedrock_ids.json` (Bedrock identity bridge). See [CLAUDE.md](CLAUDE.md) for the full architecture and contribution conventions (numbered feature branches, `#N:` commit prefix).
|
CraftDock also keeps two metadata files at the data root (bundled into world backups): `craftdock_players.json` (persistent XUID→name player directory) and `craftdock_bedrock_ids.json` (Bedrock identity bridge). See [CLAUDE.md](CLAUDE.md) for the full architecture and contribution conventions (numbered feature branches, `#N:` commit prefix).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Continuous integration and releases
|
||||||
|
|
||||||
|
CI runs on the Gitea runners (`.gitea/workflows`):
|
||||||
|
|
||||||
|
- **`ci.yml`** (branches and every Monday): Trivy on the repository (vulnerabilities, secrets, misconfiguration; the accepted `DS-0002` is documented in [`.trivyignore`](.trivyignore)), the Vitest suite inside the Dockerfile `test` stage, the arm64 image build, Trivy on the image and a smoke test (`scripts/smoke-imagem.sh`: UI served, API refuses unauthenticated requests, item icons generated, LevelDB addon loads, image healthcheck turns healthy).
|
||||||
|
- **`release.yml`** (`main` and `vX.Y.Z` tags): if the registry already holds the image for the same content (`inputs-<hash>`), nothing is rebuilt — only the new tags are applied. On a tag it creates the Gitea release with the [CHANGELOG](CHANGELOG.md) notes.
|
||||||
|
- To release: update the CHANGELOG and `package.json`, commit, then `git tag vX.Y.Z && git push gitea vX.Y.Z`.
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# CraftDock — Bedrock stack, from the published image (no local build).
|
||||||
|
# Usage: docker compose -f docker-compose.bedrock.registry.yaml up -d
|
||||||
|
# Same stack as docker-compose.bedrock.yaml, but the panel is pulled from the Gitea
|
||||||
|
# registry (built and tested by CI) instead of being built on the host.
|
||||||
|
# Secrets/overrides (${...}) are read from the project .env by docker compose.
|
||||||
|
# See the "Environment variables" table in README.md for every variable.
|
||||||
|
services:
|
||||||
|
craftdock-panel:
|
||||||
|
image: git.lucasrguerra.dev.br/lucasrguerra/craftdock:${IMAGE_TAG:-latest}
|
||||||
|
pull_policy: always
|
||||||
|
container_name: craftdock-panel
|
||||||
|
# Behind a reverse proxy (Coolify/Traefik) the panel is reached over the
|
||||||
|
# internal Docker network, so only EXPOSE the port — do NOT publish it on the
|
||||||
|
# host. In Coolify, set the service domain WITHOUT a port
|
||||||
|
# (https://your.domain, not https://your.domain:3000) and make sure the
|
||||||
|
# service's target port is 3000 — otherwise Traefik forwards to the wrong
|
||||||
|
# port and plain 443 returns "no available server".
|
||||||
|
#
|
||||||
|
# For LOCAL testing without a proxy, comment out `expose` and uncomment
|
||||||
|
# `ports` to publish on the host, then open http://localhost:3000.
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
# ports:
|
||||||
|
# - "${PORT:-3000}:3000"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- mc-data:/minecraft/data
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
# base64 of the bcrypt hash — avoids docker/Coolify eating the `$` chars.
|
||||||
|
- ADMIN_PASSWORD_HASH_B64=${ADMIN_PASSWORD_HASH_B64}
|
||||||
|
- SESSION_SECRET=${SESSION_SECRET}
|
||||||
|
- MC_CONTAINER_NAME=craftdock-mc-server
|
||||||
|
- MC_DATA_PATH=/minecraft/data
|
||||||
|
- MC_EDITION=bedrock
|
||||||
|
- MC_WORLD_NAME=worlds
|
||||||
|
- RCON_HOST=craftdock-mc-server
|
||||||
|
- RCON_PORT=25575
|
||||||
|
- RCON_PASSWORD=${RCON_PASSWORD:-} # Bedrock ignores RCON; leave empty
|
||||||
|
- MAP_VERSION=${MAP_VERSION:-}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "src/healthcheck.js"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks: [craftdock-net]
|
||||||
|
# `always` (not `unless-stopped`): the panel must come back after ANY exit —
|
||||||
|
# a crash, its own healthcheck self-restart (kill PID 1), a manual stop, or a
|
||||||
|
# host/daemon reboot. It's the control plane, so it should never stay down.
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
minecraft-server:
|
||||||
|
image: itzg/minecraft-bedrock-server:latest
|
||||||
|
container_name: craftdock-mc-server
|
||||||
|
# stdin_open + tty are REQUIRED by CraftDock: without a TTY, stdin (the Bedrock
|
||||||
|
# command channel via `docker attach`) never reaches the server, and the log
|
||||||
|
# stream is multiplexed (garbles the Console tab).
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
ports:
|
||||||
|
- "19132:19132/udp" # Bedrock game port (raw UDP — open it in the host firewall)
|
||||||
|
volumes:
|
||||||
|
- mc-data:/data
|
||||||
|
environment:
|
||||||
|
- EULA=TRUE
|
||||||
|
- OVERRIDE_SERVER_PROPERTIES=false # so the Options tab persists
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "/usr/local/bin/mc-health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 120s
|
||||||
|
networks: [craftdock-net]
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mc-data:
|
||||||
|
networks:
|
||||||
|
craftdock-net:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# CraftDock — Java stack, from the published image (no local build).
|
||||||
|
# Usage: docker compose -f docker-compose.java.registry.yaml up -d
|
||||||
|
# Same stack as docker-compose.java.yaml, but the panel is pulled from the Gitea
|
||||||
|
# registry (built and tested by CI) instead of being built on the host.
|
||||||
|
# Secrets/overrides (${...}) are read from the project .env by docker compose.
|
||||||
|
# See the "Environment variables" table in README.md for every variable.
|
||||||
|
services:
|
||||||
|
craftdock-panel:
|
||||||
|
image: git.lucasrguerra.dev.br/lucasrguerra/craftdock:${IMAGE_TAG:-latest}
|
||||||
|
pull_policy: always
|
||||||
|
container_name: craftdock-panel
|
||||||
|
# Behind a reverse proxy (Coolify/Traefik) the panel is reached over the
|
||||||
|
# internal Docker network, so only EXPOSE the port — do NOT publish it on the
|
||||||
|
# host. In Coolify, set the service domain WITHOUT a port
|
||||||
|
# (https://your.domain, not https://your.domain:3000) and make sure the
|
||||||
|
# service's target port is 3000 — otherwise Traefik forwards to the wrong
|
||||||
|
# port and plain 443 returns "no available server".
|
||||||
|
#
|
||||||
|
# For LOCAL testing without a proxy, comment out `expose` and uncomment
|
||||||
|
# `ports` to publish on the host, then open http://localhost:3000.
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
# ports:
|
||||||
|
# - "${PORT:-3000}:3000"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- mc-data:/minecraft/data
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
# base64 of the bcrypt hash — avoids docker/Coolify eating the `$` chars.
|
||||||
|
- ADMIN_PASSWORD_HASH_B64=${ADMIN_PASSWORD_HASH_B64}
|
||||||
|
- SESSION_SECRET=${SESSION_SECRET}
|
||||||
|
- MC_CONTAINER_NAME=craftdock-mc-server
|
||||||
|
- MC_DATA_PATH=/minecraft/data
|
||||||
|
- MC_EDITION=java
|
||||||
|
- MC_WORLD_NAME=world
|
||||||
|
- RCON_HOST=craftdock-mc-server
|
||||||
|
- RCON_PORT=25575
|
||||||
|
- RCON_PASSWORD=${RCON_PASSWORD:-}
|
||||||
|
- MAP_VERSION=${MAP_VERSION:-}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "src/healthcheck.js"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks: [craftdock-net]
|
||||||
|
# `always` (not `unless-stopped`): the panel must come back after ANY exit —
|
||||||
|
# a crash, its own healthcheck self-restart (kill PID 1), a manual stop, or a
|
||||||
|
# host/daemon reboot. It's the control plane, so it should never stay down.
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
minecraft-server:
|
||||||
|
image: itzg/minecraft-server:latest
|
||||||
|
container_name: craftdock-mc-server
|
||||||
|
# stdin_open + tty are REQUIRED by CraftDock: without a TTY, Docker returns a
|
||||||
|
# multiplexed log stream (8-byte frame headers) that garbles the Console tab.
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
ports:
|
||||||
|
- "25565:25565" # Java game port (raw TCP — open it in the host firewall)
|
||||||
|
volumes:
|
||||||
|
- mc-data:/data
|
||||||
|
environment:
|
||||||
|
- EULA=TRUE
|
||||||
|
- TYPE=PAPER
|
||||||
|
- VERSION=LATEST
|
||||||
|
- ENABLE_RCON=true
|
||||||
|
- RCON_PASSWORD=${RCON_PASSWORD:-} # required for the panel's Java command channel
|
||||||
|
- OVERRIDE_SERVER_PROPERTIES=false # so the Options tab persists
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "/usr/local/bin/mc-health || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 120s
|
||||||
|
networks: [craftdock-net]
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mc-data:
|
||||||
|
networks:
|
||||||
|
craftdock-net:
|
||||||
|
driver: bridge
|
||||||
Executable
+21
@@ -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"
|
||||||
Executable
+39
@@ -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
|
||||||
Executable
+66
@@ -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 ]
|
||||||
Reference in new issue
Block a user