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
This commit is contained in:
@@ -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
|
||||||
@@ -40,6 +40,12 @@ RUN apk add --no-cache tzdata ; \
|
|||||||
getent group ${GID} || addgroup --gid ${GID} ${TARGET_GROUP} ; \
|
getent group ${GID} || addgroup --gid ${GID} ${TARGET_GROUP} ; \
|
||||||
adduser -u ${UID} -g ${TARGET_GROUP} -D -S -h ${APPLICATION_HOME} ${TARGET_USER}
|
adduser -u ${UID} -g ${TARGET_GROUP} -D -S -h ${APPLICATION_HOME} ${TARGET_USER}
|
||||||
WORKDIR ${APPLICATION_HOME}
|
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}
|
USER ${TARGET_USER}
|
||||||
|
|
||||||
COPY --from=builder /tmp/bin/server /usr/bin/server
|
COPY --from=builder /tmp/bin/server /usr/bin/server
|
||||||
|
|||||||
@@ -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. |
|
| `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. |
|
| `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
|
## Credits
|
||||||
|
|
||||||
- [Ebitengine](https://github.com/hajimehoshi/ebiten) game engine by Hajime Hoshi.
|
- [Ebitengine](https://github.com/hajimehoshi/ebiten) game engine by Hajime Hoshi.
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user