831ecd0cab
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.
221 lines
8.1 KiB
YAML
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
|