Files
scrabble-game/.gitea/workflows/ci.yaml
T
Ilia Denisov 831ecd0cab
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 19s
CI / deploy (pull_request) Successful in 20s
Fix dangling config binds: seed configs to a stable host path
Root cause of the Grafana "readdirent /etc/grafana/dashboards: no such file or
directory": the CI runner checks out into an ephemeral act workspace that is
removed after the job, so binding the compose config files straight from it
dangles the mounts in the long-lived containers (verified the act source dir is
emptied after the job). caddy/otelcol/prometheus/tempo read their config once at
startup so they survive, but would break on a restart — same latent bug.

Fix (mirrors ../galaxy-game's $HOME/.galaxy-dev/monitoring): the deploy job seeds
the config dirs to a stable $HOME/.scrabble-deploy and the compose binds them via
${SCRABBLE_CONFIG_DIR:-.} (local runs keep "."). Documented in the compose header,
deploy/README.md and the ci.yaml step.
2026-06-05 17:42:21 +02:00

221 lines
8.1 KiB
YAML

name: CI
# Single gated pipeline for the test contour (Stage 16). Gitea cannot express
# cross-workflow `needs`, so the full test suite and the auto test-deploy live in
# one workflow.
#
# Branch model (CLAUDE.md): feature branches are cut from `development`; a commit
# to a feature branch triggers nothing. The pipeline runs on a PR into
# `development` or `master` (the full test suite — the merge gate) and on a push
# to `development` (after a merge). The deploy job runs only for `development`
# (PR or merge), so a PR into `master` is test-only; the prod deploy is a manual
# workflow (Stage 18).
#
# Console output is kept plain (NO_COLOR + `docker compose --ansi never` +
# `--progress plain`) so the Gitea logs stay readable.
on:
pull_request:
branches: [development, master]
push:
branches: [development]
jobs:
unit:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
env:
# The engine consumes the published scrabble-solver module from this Gitea;
# GOPRIVATE makes go fetch it directly (skipping the public proxy/checksum DB).
GOPRIVATE: gitea.iliadenisov.ru/*
DICT_VERSION: v1.0.0
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Fetch dictionary DAWGs
run: |
mkdir -p "${GITHUB_WORKSPACE}/dawg"
curl -fsSL -o /tmp/dawg.tar.gz "https://gitea.iliadenisov.ru/developer/scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz"
tar xzf /tmp/dawg.tar.gz -C "${GITHUB_WORKSPACE}/dawg"
ls -la "${GITHUB_WORKSPACE}/dawg"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.work
cache: true
- name: gofmt
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "gofmt needed on:"; echo "$unformatted"; exit 1
fi
- name: vet
run: go vet ./backend/... ./pkg/... ./gateway/... ./platform/telegram/...
- name: build
run: go build ./backend/... ./pkg/... ./gateway/... ./platform/telegram/...
- name: test
env:
BACKEND_DICT_DIR: ${{ github.workspace }}/dawg
run: go test -count=1 ./backend/... ./pkg/... ./gateway/... ./platform/telegram/...
integration:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
env:
# Ryuk (testcontainers' reaper) does not start cleanly on every runner; the
# suite's TestMain terminates its own container, so disable it.
TESTCONTAINERS_RYUK_DISABLED: "true"
GOPRIVATE: gitea.iliadenisov.ru/*
DICT_VERSION: v1.0.0
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Fetch dictionary DAWGs
run: |
mkdir -p "${GITHUB_WORKSPACE}/dawg"
curl -fsSL -o /tmp/dawg.tar.gz "https://gitea.iliadenisov.ru/developer/scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz"
tar xzf /tmp/dawg.tar.gz -C "${GITHUB_WORKSPACE}/dawg"
ls -la "${GITHUB_WORKSPACE}/dawg"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.work
cache: true
- name: Integration tests
# -count=1 disables the cache; -p=1 -parallel=1 keeps the container-backed
# tests serial; the 15-minute timeout bounds a stuck container pull.
env:
BACKEND_DICT_DIR: ${{ github.workspace }}/dawg
run: go test -tags=integration -count=1 -p=1 -parallel=1 -timeout=15m ./backend/...
ui:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: ui
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install pnpm
run: npm install -g pnpm@11.0.9
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Type-check
run: pnpm run check
- name: Unit tests
run: pnpm run test:unit
- name: Build
run: pnpm run build
- name: Bundle-size budget
run: node scripts/bundle-size.mjs
- name: Install Playwright browsers
run: pnpm exec playwright install chromium webkit
timeout-minutes: 5
- name: E2E smoke (mock)
run: pnpm run test:e2e
timeout-minutes: 5
deploy:
# Auto test-deploy on a PR into development and on the push that merges it.
# A PR into master is test-only (this job is skipped); prod deploy is manual.
needs: [unit, integration, ui]
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/development') || (github.event_name == 'pull_request' && github.base_ref == 'development') }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
env:
NO_COLOR: "1"
DOCKER_CLI_HINTS: "false"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build and (re)deploy the test contour
working-directory: deploy
env:
# Sensitive values -> secrets; non-sensitive -> variables. The compose
# interpolates these unprefixed names (see deploy/.env.example).
POSTGRES_PASSWORD: ${{ secrets.TEST_POSTGRES_PASSWORD }}
AWG_CONF: ${{ secrets.TEST_AWG_CONF }}
GM_BASICAUTH_HASH: ${{ secrets.TEST_GM_BASICAUTH_HASH }}
GRAFANA_ADMIN_PASSWORD: ${{ secrets.TEST_GRAFANA_ADMIN_PASSWORD }}
TELEGRAM_BOT_TOKEN_EN: ${{ secrets.TEST_TELEGRAM_BOT_TOKEN_EN }}
TELEGRAM_BOT_TOKEN_RU: ${{ secrets.TEST_TELEGRAM_BOT_TOKEN_RU }}
GM_BASICAUTH_USER: ${{ vars.TEST_GM_BASICAUTH_USER }}
GRAFANA_ROOT_URL: ${{ vars.TEST_GRAFANA_ROOT_URL }}
CADDY_SITE_ADDRESS: ${{ vars.TEST_CADDY_SITE_ADDRESS }}
TELEGRAM_MINIAPP_URL: ${{ vars.TEST_TELEGRAM_MINIAPP_URL }}
TELEGRAM_GAME_CHANNEL_ID_EN: ${{ vars.TEST_TELEGRAM_GAME_CHANNEL_ID_EN }}
TELEGRAM_GAME_CHANNEL_ID_RU: ${{ vars.TEST_TELEGRAM_GAME_CHANNEL_ID_RU }}
# The test contour always uses Telegram's test environment — pinned here,
# not an operator variable. Stage 18's prod workflow leaves it false.
TELEGRAM_TEST_ENV: "true"
VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }}
VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }}
VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }}
GATEWAY_DEFAULT_SUPPORTED_LANGUAGES: ${{ vars.TEST_GATEWAY_DEFAULT_SUPPORTED_LANGUAGES }}
# Unset vars render empty -> the compose ":-" defaults apply.
POSTGRES_DB: ${{ vars.TEST_POSTGRES_DB }}
POSTGRES_USER: ${{ vars.TEST_POSTGRES_USER }}
DICT_VERSION: ${{ vars.TEST_DICT_VERSION }}
LOG_LEVEL: ${{ vars.TEST_LOG_LEVEL }}
run: |
# Seed the config files to a stable host path. The runner checks out into
# an ephemeral act workspace that is removed after the job, which would
# dangle the compose config bind mounts in the long-lived containers
# (e.g. Grafana then logs "no such file or directory"). Bind from a stable
# dir instead (mirrors ../galaxy-game's $HOME/.galaxy-dev/monitoring).
conf="$HOME/.scrabble-deploy"
rm -rf "$conf"
mkdir -p "$conf"
cp -r caddy otelcol prometheus tempo grafana "$conf"/
export SCRABBLE_CONFIG_DIR="$conf"
docker compose --ansi never build --progress plain
docker compose --ansi never up -d --remove-orphans
- name: Probe the gateway through caddy
run: |
set -u
for i in $(seq 1 20); do
if docker run --rm --network edge alpine:3.20 wget -q -T 5 -O /dev/null http://scrabble/; then
echo "healthy: GET http://scrabble/"
exit 0
fi
sleep 3
done
echo "probe failed; recent gateway logs:"
docker logs --tail 50 scrabble-gateway || true
exit 1
- name: Prune dangling images
if: always()
run: docker image prune -f