From 0e3e3b6c90685acb4f31f44ed314fc6678050300 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 2 Jun 2026 11:45:28 +0200 Subject: [PATCH] Add Gitea Actions CI/CD and deploy descriptor - .gitea/workflows/ci.yaml: test on PR/push to master, deploy on push to master - deploy/docker-compose.yml: 15-puzzle container on the edge network with a persistent data volume - Dockerfile: create /data owned by app for the DATA_FILE volume - README: document deployment, required Actions secrets/variables, and persistence --- .gitea/workflows/ci.yaml | 91 +++++++++++++++++++++++++++++++++++++++ Dockerfile | 6 +++ README.md | 34 +++++++++++++++ deploy/docker-compose.yml | 34 +++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 deploy/docker-compose.yml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..cc500a9 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,91 @@ +name: CI + +# Tests and deploys the game into the host Docker daemon. +# +# * pull_request -> master : runs the server test suite (gates the merge). +# * push -> master (a merge): runs the tests, then rebuilds the image and +# (re)deploys the `15-puzzle` container via deploy/docker-compose.yml. +# +# The runner is host-mode, so docker / docker compose / git are available +# directly and the image is built locally on the host daemon (no registry) — +# the same pattern as ../galaxy-game/.gitea/workflows/dev-deploy.yaml. + +on: + push: + branches: [master] + paths-ignore: ['**.md'] + pull_request: + branches: [master] + paths-ignore: ['**.md'] + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run Go tests + # internal/puzzle and cmd/ui pull in desktop Ebitengine, which needs C + # libraries (alsa, libX11, Xrandr) that are not installed on the runner + # host. We test the server-side packages instead — exactly the code that + # runs inside the deployed container (HMAC validation, file repository, + # HTTP API). The wasm build is exercised by docker build in the deploy job. + run: go test -count=1 ./internal/repo/... ./internal/validator/... ./internal/web-service/... + + deploy: + needs: test + if: ${{ gitea.event_name == 'push' }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image and (re)deploy container + working-directory: deploy + env: + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + ACCESS_CODE: ${{ secrets.ACCESS_CODE }} + PROJECT_LINK: ${{ vars.PROJECT_LINK }} + DATA_FILE: ${{ vars.DATA_FILE }} + SERVER_PORT: ${{ vars.SERVER_PORT }} + CONTEXT_ROOT: ${{ vars.CONTEXT_ROOT }} + run: docker compose up -d --build --remove-orphans + + - name: Probe the container + env: + SERVER_PORT: ${{ vars.SERVER_PORT }} + CONTEXT_ROOT: ${{ vars.CONTEXT_ROOT }} + run: | + set -u + port="${SERVER_PORT:-8080}" + root="${CONTEXT_ROOT:-/}"; root="/${root#/}"; root="${root%/}" + url="http://15-puzzle:${port}${root}/puzzle.html" + echo "probing ${url}" + for i in $(seq 1 15); do + if docker run --rm --network edge alpine:3.20 \ + wget -q -T 5 -O /dev/null "${url}"; then + echo "healthy: ${url}" + exit 0 + fi + sleep 2 + done + echo "probe failed for ${url}; recent container logs:" + docker logs --tail 50 15-puzzle || true + exit 1 + + - name: Prune dangling images + if: always() + run: docker image prune -f diff --git a/Dockerfile b/Dockerfile index b84fa05..06521bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,12 @@ RUN apk add --no-cache tzdata ; \ getent group ${GID} || addgroup --gid ${GID} ${TARGET_GROUP} ; \ adduser -u ${UID} -g ${TARGET_GROUP} -D -S -h ${APPLICATION_HOME} ${TARGET_USER} WORKDIR ${APPLICATION_HOME} + +# Persisted game-data directory (DATA_FILE lives here). A named volume mounted +# at /data inherits this ownership on first use, so the unprivileged app user +# can write the temp-file + rename that repo.go performs. +RUN mkdir -p /data && chown ${TARGET_USER}:${TARGET_GROUP} /data + USER ${TARGET_USER} COPY --from=builder /tmp/bin/server /usr/bin/server diff --git a/README.md b/README.md index 4cd7123..fa07be1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,40 @@ Certain environment variables should be set for the server to start: | `CONTEXT_ROOT` | Server requests URI root path, defaulting to `/` if not set. | | `STATIC_DIR` | Directory where static files are located, defaulting to the current directory if not set. | +## Deployment + +The repository ships a [Gitea Actions](https://docs.gitea.com/usage/actions/overview) +workflow ([`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml)) that runs the +server test suite on every pull request to `master`, and on every push to +`master` additionally rebuilds the Docker image and (re)deploys the container on +the host Docker daemon through [`deploy/docker-compose.yml`](deploy/docker-compose.yml). + +The deploy job runs in host mode: it builds the image locally (no registry) and +recreates the `15-puzzle` container attached to the external `edge` Docker +network, so a host reverse proxy can route to `15-puzzle:8080`. + +### Required Actions secrets and variables + +Configure these under the repository's *Settings → Actions*: + +| Kind | Name | Example / note | +| - | - | - | +| Secret | `BOT_TOKEN` | Telegram Bot API token. | +| Secret | `ACCESS_CODE` | Pin-code for the Statistics Screen. | +| Variable | `PROJECT_LINK` | URL to the project's source code. | +| Variable | `DATA_FILE` | `/data/data.json` (lives on the persistent volume). | +| Variable | `SERVER_PORT` | `8080`. | +| Variable | `CONTEXT_ROOT` | `/`. | + +`STATIC_DIR` is intentionally left unset: the static assets (HTML, `game.wasm`, +`wasm_exec.js`) are baked into the image and served from the working directory. + +### Persistence + +Game data is stored on the named Docker volume `15-puzzle-data` mounted at +`/data`, so player ratings survive container recreation. The image creates +`/data` owned by the unprivileged `app` user so the volume is writable. + ## Credits - [Ebitengine](https://github.com/hajimehoshi/ebiten) game engine by Hajime Hoshi. diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..209c10a --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,34 @@ +# Deploy descriptor for the 15-puzzle container on the host Docker daemon. +# Driven by .gitea/workflows/ci.yaml (`docker compose up -d --build`); env +# values are interpolated from Gitea Actions secrets/variables exported by the +# deploy job. Joins the external `edge` network so the host reverse proxy can +# route to `15-puzzle:8080`. +name: 15-puzzle + +services: + app: + container_name: 15-puzzle + image: 15-puzzle:latest + build: + context: .. + dockerfile: Dockerfile + restart: unless-stopped + environment: + BOT_TOKEN: ${BOT_TOKEN:?set BOT_TOKEN} + ACCESS_CODE: ${ACCESS_CODE:?set ACCESS_CODE} + PROJECT_LINK: ${PROJECT_LINK:?set PROJECT_LINK} + DATA_FILE: ${DATA_FILE:-/data/data.json} + SERVER_PORT: ${SERVER_PORT:-8080} + CONTEXT_ROOT: ${CONTEXT_ROOT:-/} + volumes: + - data:/data + networks: + - edge + +volumes: + data: + name: 15-puzzle-data + +networks: + edge: + external: true