Stage 16: deploy infra & test contour
- backend + gateway multi-stage distroless Dockerfiles; the gateway embeds and
serves the SPA at / and /telegram/ via go:embed (committed dist placeholder,
real build baked in by the image's node stage)
- deploy/docker-compose.yml: backend + gateway + Postgres + Telegram connector
(VPN sidecar) + OTel Collector + Prometheus (15d) + Tempo (72h) + Grafana,
fronted by a caddy owning a single /_gm Basic-Auth (admin console + Grafana
subpath); inter-service on a private network, only caddy on the edge network
- new metrics: backend accounts_created_total{kind} (robots excluded) and an
in-memory gateway active_users{window=24h,7d} gauge
- CI: single .gitea/workflows/ci.yaml (unit/integration/ui + a gated test-contour
deploy) on the new feature/* -> development -> master branch model; the old
go-unit/integration/ui-test workflows are folded in; the connector-scoped
compose is retired (superseded by deploy/)
- docs: ARCHITECTURE §11/§12/§13, root + gateway READMEs, CLAUDE.md branching,
PLAN.md (stage 16 done + refinements + Stage 17 forward-notes)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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 17).
|
||||
#
|
||||
# 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 }}
|
||||
TELEGRAM_TEST_ENV: ${{ vars.TEST_TELEGRAM_TEST_ENV }}
|
||||
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 }}
|
||||
run: |
|
||||
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
|
||||
@@ -1,81 +0,0 @@
|
||||
name: Tests · Go
|
||||
|
||||
# Fast unit tests for the Go side of the monorepo. Runs on every push and pull
|
||||
# request whose path filter matches a Go source directory. The module list
|
||||
# grows as new go.work modules (gateway, pkg/*, platform/*) are added by later
|
||||
# stages.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'gateway/**'
|
||||
- 'pkg/**'
|
||||
- 'platform/**'
|
||||
- 'go.work'
|
||||
- 'go.work.sum'
|
||||
- '.gitea/workflows/go-unit.yaml'
|
||||
- '!**/*.md'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'gateway/**'
|
||||
- 'pkg/**'
|
||||
- 'platform/**'
|
||||
- 'go.work'
|
||||
- 'go.work.sum'
|
||||
- '.gitea/workflows/go-unit.yaml'
|
||||
- '!**/*.md'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
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).
|
||||
# DICT_VERSION selects the dictionary DAWG release the engine tests load.
|
||||
GOPRIVATE: gitea.iliadenisov.ru/*
|
||||
DICT_VERSION: v1.0.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch dictionary DAWGs
|
||||
# The DAWGs moved to the scrabble-dictionary repo (the solver is now a
|
||||
# versioned module pinned in backend/go.mod, fetched via GOPRIVATE — no
|
||||
# sibling clone). They ship as a release artifact, one semver per set.
|
||||
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
|
||||
# -count=1 disables the test cache so a green run never depends on a
|
||||
# previous runner's cached state. BACKEND_DICT_DIR points the engine
|
||||
# tests at the DAWGs fetched from the dictionary release.
|
||||
env:
|
||||
BACKEND_DICT_DIR: ${{ github.workspace }}/dawg
|
||||
run: go test -count=1 ./backend/... ./pkg/... ./gateway/... ./platform/telegram/...
|
||||
@@ -1,71 +0,0 @@
|
||||
name: Tests · Integration
|
||||
|
||||
# Postgres-backed integration tests for the Go backend, gated behind the
|
||||
# `integration` build tag. They spin a throwaway postgres:17-alpine container via
|
||||
# testcontainers-go, which reaches the host Docker daemon through the socket the
|
||||
# Gitea runner exposes. Slower than the unit job (go-unit.yaml); run serially
|
||||
# (-p=1) with Ryuk disabled — TestMain terminates its own container. The module
|
||||
# list grows as new go.work modules are added by later stages.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'pkg/**'
|
||||
- 'go.work'
|
||||
- 'go.work.sum'
|
||||
- '.gitea/workflows/integration.yaml'
|
||||
- '!**/*.md'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'pkg/**'
|
||||
- 'go.work'
|
||||
- 'go.work.sum'
|
||||
- '.gitea/workflows/integration.yaml'
|
||||
- '!**/*.md'
|
||||
|
||||
jobs:
|
||||
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"
|
||||
# The engine consumes the published scrabble-solver module from this Gitea
|
||||
# (GOPRIVATE -> direct fetch, skipping the public proxy/checksum DB);
|
||||
# DICT_VERSION selects the dictionary DAWG release the engine tests load.
|
||||
GOPRIVATE: gitea.iliadenisov.ru/*
|
||||
DICT_VERSION: v1.0.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch dictionary DAWGs
|
||||
# The DAWGs moved to the scrabble-dictionary repo (the solver is now a
|
||||
# versioned module pinned in backend/go.mod, fetched via GOPRIVATE — no
|
||||
# sibling clone). They ship as a release artifact; the engine's untagged
|
||||
# tests (compiled here too) load them.
|
||||
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 test cache; -p=1 -parallel=1 keeps the
|
||||
# container-backed tests serial; the 15-minute timeout bounds a stuck
|
||||
# container pull. The engine package's (untagged) tests also compile and
|
||||
# run here, so BACKEND_DICT_DIR points them at the DAWGs from the release.
|
||||
env:
|
||||
BACKEND_DICT_DIR: ${{ github.workspace }}/dawg
|
||||
run: go test -tags=integration -count=1 -p=1 -parallel=1 -timeout=15m ./backend/...
|
||||
@@ -1,67 +0,0 @@
|
||||
name: Tests · UI
|
||||
|
||||
# Hermetic UI checks: type-check, Vitest unit tests, production build with a
|
||||
# bundle-size budget, and a Playwright smoke (Chromium + WebKit) against the in-memory
|
||||
# mock transport (no backend/gateway/Postgres). The committed src/gen/ codegen is built, not
|
||||
# regenerated (the same model as the Go committed jet/fbs output).
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'ui/**'
|
||||
- '.gitea/workflows/ui-test.yaml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'ui/**'
|
||||
- '.gitea/workflows/ui-test.yaml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
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
|
||||
|
||||
# The Playwright system libraries are provisioned once on the runner host
|
||||
# (`sudo npx playwright@<version> install-deps chromium`), so the job needs no
|
||||
# apt and no sudo: it only downloads the browser binaries into the runner cache
|
||||
# (persisted by the host executor) and runs the suite. WebKit's Debian build
|
||||
# bundles most of its own libraries and runs headless without extra host deps; if
|
||||
# a runner ever lacks one, provision it once on the host with
|
||||
# `sudo npx playwright install-deps webkit`. The timeouts guard against a future
|
||||
# hang. Keep this in lockstep with @playwright/test in package.json — re-run
|
||||
# install-deps on the host after a major bump.
|
||||
- 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
|
||||
Reference in New Issue
Block a user