phase 2: ui testing infrastructure

Vitest + @testing-library/jest-dom matchers wired through tests/setup.ts.
Playwright with four projects: chromium-desktop, webkit-desktop,
chromium-mobile-iphone-13, chromium-mobile-pixel-5; traces and
screenshots retained on failure.

.gitea/workflows/ui-test.yaml runs Tier 1 on every push and pull
request: monorepo Go service tests (backend with -p 1 to dodge
testcontainer contention; gateway, game, every pkg/<name> module),
pnpm install --frozen-lockfile, playwright install --with-deps,
pnpm test, pnpm exec playwright test. Uploads playwright-report
and test-results on failure. Integration suite stays gated behind
make -C integration integration; deprecated client/ excluded.

.gitea/workflows/ui-release.yaml mirrors Tier 1 on v* tag push and
keeps commented placeholders for visual regression (Phase 33) and
macOS iOS smoke (Phase 32).

ui/docs/testing.md documents both tiers and the local invocations
that mirror what CI runs. ui/PLAN.md Phase 2 marked done; Phase 3
gains a bullet to extend the go test command with ./ui/core/...;
Phase 36 has the renamed release workflow path.

tools/local-ci/ ships a self-contained docker-compose for verifying
workflows against a local Gitea + arm64 act_runner before pushing
to a real instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-07 08:24:44 +02:00
parent cf41be9eff
commit 7450006ed3
17 changed files with 885 additions and 15 deletions
+1
View File
@@ -0,0 +1 @@
.env
+42
View File
@@ -0,0 +1,42 @@
.PHONY: help up down logs status clean push
.DEFAULT_GOAL := help
COMPOSE := docker compose
GITEA_USER := galaxy
GITEA_PASS := galaxy-dev
REPO_NAME := galaxy
REMOTE_NAME := local-gitea
REPO_ROOT := $(realpath $(CURDIR)/../..)
GIT := git -C $(REPO_ROOT)
REMOTE_URL := http://$(GITEA_USER):$(GITEA_PASS)@localhost:3000/$(GITEA_USER)/$(REPO_NAME).git
help:
@echo "Local Gitea CI for galaxy:"
@echo " make up Bring up Gitea + runner (idempotent)"
@echo " make down Stop both containers"
@echo " make logs Tail logs"
@echo " make status Show container status"
@echo " make push Push current branch to local Gitea"
@echo " make clean Stop and wipe all local state"
up:
@./bootstrap.sh
down:
$(COMPOSE) down
logs:
$(COMPOSE) logs -f --tail=50
status:
$(COMPOSE) ps
push:
@$(GIT) remote get-url $(REMOTE_NAME) >/dev/null 2>&1 || \
$(GIT) remote add $(REMOTE_NAME) $(REMOTE_URL)
$(GIT) push $(REMOTE_NAME) HEAD
clean:
$(COMPOSE) down -v
rm -f .env
+98
View File
@@ -0,0 +1,98 @@
# Local Gitea CI
Self-contained Gitea + Actions runner for verifying
`.gitea/workflows/*` honestly before pushing to a real Gitea instance.
Runs natively on arm64 (Apple Silicon) — every image below has an
arm64 variant, so Docker pulls the right architecture and the runner
executes workflow steps without QEMU emulation.
## Prerequisites
- Docker (Colima or Docker Desktop)
- `python3`, `curl`, `bash` — all built into macOS
## First time
```sh
make -C tools/local-ci up
```
This:
1. brings up the Gitea container;
2. creates an admin user (`galaxy` / `galaxy-dev`);
3. creates the `galaxy/galaxy` repo;
4. fetches a runner registration token from the Gitea API;
5. brings up the runner with that token (the runner persists its
credentials in a Docker volume and ignores the token on subsequent
restarts).
The script is idempotent — re-running it is safe.
## Pushing a branch
```sh
make -C tools/local-ci push
```
This adds a `local-gitea` remote on the first run and then pushes the
current `HEAD`. Equivalent manual flow:
```sh
git remote add local-gitea \
http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git
git push local-gitea HEAD
```
The Tier 1 workflow fires on `push` to any branch and the Tier 2
workflow fires on tags matching `v*`. Watch runs at:
<http://localhost:3000/galaxy/galaxy/actions>
## Operational targets
| Target | What it does |
| ---------------- | -------------------------------------------- |
| `make up` | Bring up Gitea + runner (idempotent) |
| `make down` | Stop both containers (state preserved) |
| `make logs` | Tail logs from both containers |
| `make status` | Show container status |
| `make push` | Push current `HEAD` to local Gitea |
| `make clean` | Stop and wipe all local state (full reset) |
## What's in the box
| Component | Image | Role |
| ---------- | ---------------------------------- | ------------------------------------------- |
| Gitea | `gitea/gitea:1.23` | Server with SQLite backend |
| act_runner | `gitea/act_runner:0.6.1` | Single-capacity runner registered on boot |
| Workflow | `catthehacker/ubuntu:act-latest` | Image spawned per job (multi-arch) |
The runner mounts the host Docker socket and spawns workflow
containers on the same Docker network as Gitea, so
`actions/checkout` reaches the server at `http://gitea:3000` from
inside spawned containers.
## Caveats
- Gitea's `ROOT_URL` is set to `http://gitea:3000/` so spawned
workflow containers reach the server through the compose network.
The web UI works at `http://localhost:3000` via port mapping, but
copy-paste URLs in the UI may show `gitea:3000` instead of
`localhost:3000`. Harmless for local dev; switch the host part by
hand when copying.
- The runner is single-capacity (`runner.capacity: 1` in
`config.yaml`). Concurrent jobs queue. Bump if you need parallel
jobs.
- First push from a fresh checkout uploads the full repo history
(~tens of MB). Subsequent pushes are deltas.
- `actions/upload-artifact@v4` requires Gitea ≥ 1.21 — we pin
`1.23` to stay above the cutoff.
- Workflow steps run as `root` inside the spawned container; this
matches the upstream catthehacker behaviour. Keep that in mind if
you add steps that touch host-mounted directories.
- On Apple Silicon the runner image and its catthehacker child run
natively as arm64. Some pre-built tools that ship in the image are
amd64-only and would fall back to QEMU; `setup-go`, `setup-node`,
and `pnpm/action-setup` all download arm64 binaries themselves, so
the workflow steps we care about stay native.
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Bring up Gitea, create the admin user and the galaxy/galaxy repo,
# fetch a runner registration token, bring up the runner.
# Idempotent — re-runnable.
set -euo pipefail
cd "$(dirname "$0")"
GITEA_USER=galaxy
GITEA_PASS=galaxy-dev
GITEA_EMAIL=galaxy@local
REPO_NAME=galaxy
GITEA_URL=http://localhost:3000
echo ">>> Bringing up Gitea..."
docker compose up -d gitea
echo ">>> Waiting for Gitea API..."
for _ in $(seq 1 120); do
if curl -fsS "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; then
echo "Gitea is up."
break
fi
sleep 1
done
if ! curl -fsS "${GITEA_URL}/api/v1/version" >/dev/null 2>&1; then
echo "Gitea did not come up within 120 seconds." >&2
docker compose logs gitea | tail -30 >&2
exit 1
fi
echo ">>> Creating admin user (idempotent)..."
docker compose exec -T gitea su git -c "
gitea admin user create \
--username ${GITEA_USER} \
--password ${GITEA_PASS} \
--email ${GITEA_EMAIL} \
--admin \
--must-change-password=false 2>&1 || true
"
echo ">>> Creating repo (idempotent)..."
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
-u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${REPO_NAME}\",\"private\":true,\"auto_init\":false}" \
"${GITEA_URL}/api/v1/user/repos")
case "${HTTP_CODE}" in
201) echo "Repo created." ;;
409) echo "Repo already exists." ;;
*)
echo "Unexpected response (${HTTP_CODE}) creating repo." >&2
exit 1
;;
esac
echo ">>> Fetching runner registration token..."
RUNNER_TOKEN=$(curl -fsS \
-u "${GITEA_USER}:${GITEA_PASS}" \
"${GITEA_URL}/api/v1/admin/runners/registration-token" \
| python3 -c "import json, sys; print(json.load(sys.stdin)['token'])")
# act_runner uses RUNNER_TOKEN only on the first boot. After registration
# it persists credentials in the named runner-data volume (/data/.runner)
# and ignores the env token on subsequent restarts. Writing a fresh token
# every time is harmless.
echo "RUNNER_TOKEN=${RUNNER_TOKEN}" > .env
echo ">>> Bringing up runner..."
docker compose up -d runner
cat <<EOF
Done.
Push the current branch and watch a run:
cd $(cd ../.. && pwd)
git remote add local-gitea http://${GITEA_USER}:${GITEA_PASS}@localhost:3000/${GITEA_USER}/${REPO_NAME}.git 2>/dev/null || true
git push local-gitea HEAD
open http://localhost:3000/${GITEA_USER}/${REPO_NAME}/actions
Or use \`make push\` from this directory.
EOF
+35
View File
@@ -0,0 +1,35 @@
# act_runner configuration.
#
# The `ubuntu-latest` label is mapped to catthehacker/ubuntu:act-latest,
# which is multi-arch — Docker on Apple Silicon pulls the arm64 variant
# and runs it natively (no QEMU). The same image is what `act` uses
# locally, so workflows behave the same.
log:
level: info
runner:
file: /data/.runner
capacity: 1
fetch_timeout: 5s
fetch_interval: 2s
labels:
- "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
cache:
enabled: true
dir: /data/cache
container:
# Spawned workflow containers join the same network as Gitea so
# actions/checkout and other steps can reach the server at
# http://gitea:3000.
network: galaxy-local-gitea-net
privileged: false
options: ""
workdir_parent: ""
valid_volumes: []
force_pull: false
host:
workdir_parent: ""
+78
View File
@@ -0,0 +1,78 @@
# Local Gitea + Actions runner for verifying .gitea/workflows/*.
# Runs natively on arm64 (Apple Silicon) — every image below is multi-arch.
#
# Browser: http://localhost:3000
# API: http://localhost:3000/api/v1
# Push URL: http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git
# Actions: http://localhost:3000/galaxy/galaxy/actions
#
# `bootstrap.sh` (or `make up`) brings everything up and registers the
# runner. State persists in named Docker volumes; `make clean` wipes them.
services:
gitea:
image: gitea/gitea:1.23
container_name: galaxy-local-gitea
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: sqlite3
GITEA__database__PATH: /data/gitea/gitea.db
# ROOT_URL uses the in-network hostname so the runner and spawned
# workflow containers reach Gitea through the compose network.
# The browser still works at http://localhost:3000 via the port
# mapping below; UI-generated copy URLs may show "gitea:3000",
# which is harmless for local dev.
GITEA__server__ROOT_URL: http://gitea:3000/
GITEA__server__SSH_PORT: "2222"
GITEA__actions__ENABLED: "true"
GITEA__security__INSTALL_LOCK: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
ports:
- "3000:3000"
- "2222:22"
volumes:
- gitea-data:/data
networks:
- gitea-net
healthcheck:
test:
- CMD-SHELL
- wget -q -O- http://localhost:3000/api/v1/version >/dev/null || exit 1
interval: 5s
timeout: 3s
retries: 30
start_period: 5s
runner:
image: gitea/act_runner:0.6.1
container_name: galaxy-local-runner
restart: unless-stopped
depends_on:
gitea:
condition: service_healthy
environment:
CONFIG_FILE: /config/config.yaml
GITEA_INSTANCE_URL: http://gitea:3000
# Provided by bootstrap.sh in the .env file. After the first
# successful registration, act_runner persists credentials in
# /data/.runner and ignores this token on subsequent restarts.
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN:-}
GITEA_RUNNER_NAME: galaxy-local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner-data:/data
- ./config.yaml:/config/config.yaml:ro
networks:
- gitea-net
networks:
gitea-net:
name: galaxy-local-gitea-net
volumes:
gitea-data:
name: galaxy-local-gitea-data
runner-data:
name: galaxy-local-runner-data