Add Gitea Actions CI/CD and deploy descriptor
CI / test (pull_request) Successful in 1m20s
CI / deploy (pull_request) Has been skipped

- .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
This commit is contained in:
Ilia Denisov
2026-06-02 11:45:28 +02:00
parent 0910691e3c
commit 0e3e3b6c90
4 changed files with 165 additions and 0 deletions
+91
View File
@@ -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
+6
View File
@@ -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
+34
View File
@@ -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.
+34
View File
@@ -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