diff --git a/.gitea/workflows/deploy-prod.yaml b/.gitea/workflows/deploy-prod.yaml new file mode 100644 index 0000000..7514a3c --- /dev/null +++ b/.gitea/workflows/deploy-prod.yaml @@ -0,0 +1,31 @@ +name: deploy-prod + +# Placeholder for the production rollout workflow. Today it only proves +# the manual entry point works; the actual `docker save | ssh prod +# docker load` + remote `docker compose up -d` pipeline is wired in +# once the production host, SSH credentials, and DNS are decided. + +on: + workflow_dispatch: + inputs: + image_tag: + description: "Image tag to deploy (commit-, produced by prod-build.yaml)" + required: true + type: string + +jobs: + deploy: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Announce target + run: | + echo "Would deploy image tag: ${{ inputs.image_tag }}" + echo "TODO:" + echo " 1. Download galaxy-images-${{ inputs.image_tag }} from prod-build artifacts." + echo " 2. scp the .tar.gz bundles to the production host." + echo " 3. ssh prod 'docker load -i ...' for backend / gateway / engine." + echo " 4. ssh prod 'docker compose -f /opt/galaxy/docker-compose.yml up -d'." + echo " 5. Probe https://api.galaxy.com/healthz and roll back on failure." diff --git a/.gitea/workflows/dev-deploy.yaml b/.gitea/workflows/dev-deploy.yaml new file mode 100644 index 0000000..91f7dce --- /dev/null +++ b/.gitea/workflows/dev-deploy.yaml @@ -0,0 +1,116 @@ +name: dev-deploy + +# Builds the Galaxy stack and (re)deploys it into the long-lived dev +# environment on the host running this Gitea Actions runner. Triggered +# on every merge into `development`. Branch protections on `development` +# guarantee the commit already passed `go-unit`, `ui-test`, and +# `integration` as part of the PR that produced this push, so this +# workflow does not re-run those tests — it focuses on packaging and +# rollout. + +on: + push: + branches: + - development + paths: + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'ui/**' + - 'go.work' + - 'go.work.sum' + - 'tools/dev-deploy/**' + - '.gitea/workflows/dev-deploy.yaml' + - '!**/*.md' + +jobs: + deploy: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.work + cache: true + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.0.7 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ui/pnpm-lock.yaml + + - name: Install UI dependencies + working-directory: ui + run: pnpm install --frozen-lockfile + + - name: Build UI frontend + working-directory: ui/frontend + env: + VITE_GATEWAY_BASE_URL: https://api.galaxy.lan + run: | + # The response-signing public key is committed in + # `.env.development` alongside its private counterpart in + # `tools/local-dev/keys/`. Pull it from there at build time so + # the production-mode bundle ships the same key the dev + # gateway uses to sign. + export VITE_GATEWAY_RESPONSE_PUBLIC_KEY="$(grep -E '^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=' .env.development | cut -d= -f2)" + pnpm build + + - name: Build galaxy-engine image + working-directory: ${{ gitea.workspace }} + run: | + docker build \ + -t galaxy-engine:dev \ + -f game/Dockerfile \ + . + + - name: Build backend + gateway images + working-directory: tools/dev-deploy + run: | + docker compose build galaxy-backend galaxy-api + + - name: Seed UI volume + run: | + docker volume create galaxy-dev-ui-dist >/dev/null + docker run --rm \ + -v galaxy-dev-ui-dist:/dst \ + -v "${{ gitea.workspace }}/ui/frontend/build:/src:ro" \ + alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/' + + - name: Bring up the stack + working-directory: tools/dev-deploy + env: + GALAXY_DEV_GAME_STATE_DIR: ${{ env.HOME }}/.galaxy-dev/game-state + run: | + mkdir -p "$GALAXY_DEV_GAME_STATE_DIR" + docker compose up -d --wait --remove-orphans + + - name: Probe the stack + run: | + set -e + # Use --resolve so the probe goes through the same routing as + # a browser on the host: the host Caddy on :443 (which has + # `tls internal`) terminates and forwards into the edge + # network. We accept the host's internal CA via -k because + # the runner image has no reason to trust it. + curl -sk --max-time 10 https://api.galaxy.lan/healthz \ + | tee /tmp/healthz + test -s /tmp/healthz + curl -sk --max-time 10 -o /dev/null -w '%{http_code}\n' \ + https://www.galaxy.lan/ | tee /tmp/www_status + grep -qE '^(200|304)$' /tmp/www_status diff --git a/.gitea/workflows/go-unit.yaml b/.gitea/workflows/go-unit.yaml new file mode 100644 index 0000000..c33d1dd --- /dev/null +++ b/.gitea/workflows/go-unit.yaml @@ -0,0 +1,78 @@ +name: go-unit + +# 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 integration suite (testcontainers-driven, slow) lives in +# `integration.yaml` and only fires for PRs into `development`/`main` +# and pushes to `development`. + +on: + push: + paths: + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'ui/core/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/go-unit.yaml' + - '!**/*.md' + pull_request: + paths: + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'ui/core/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/go-unit.yaml' + - '!**/*.md' + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.work + cache: true + + - name: Run Go tests + # client/ is the deprecated Fyne client; excluded from CI per + # ui/PLAN.md §74. -count=1 disables Go's test cache so a green + # run never depends on a previous runner's cached state. The + # backend suite is run with -p 1 because most backend packages + # spawn their own Postgres testcontainer, and parallel + # Postgres bootstraps starve each other on a constrained + # runner. pkg modules are listed one by one because ./pkg/... + # does not recurse across the independent go.work modules + # under pkg/. + run: | + go test -count=1 -p 1 ./backend/... + go test -count=1 \ + ./gateway/... \ + ./game/... \ + ./ui/core/... \ + ./pkg/calc/... \ + ./pkg/connector/... \ + ./pkg/cronutil/... \ + ./pkg/error/... \ + ./pkg/geoip/... \ + ./pkg/model/... \ + ./pkg/postgres/... \ + ./pkg/redisconn/... \ + ./pkg/schema/... \ + ./pkg/storage/... \ + ./pkg/transcoder/... \ + ./pkg/util/... diff --git a/.gitea/workflows/integration.yaml b/.gitea/workflows/integration.yaml new file mode 100644 index 0000000..1f94fa8 --- /dev/null +++ b/.gitea/workflows/integration.yaml @@ -0,0 +1,65 @@ +name: integration + +# Full integration suite (testcontainers-driven, ~5–10 minutes). Heavy +# enough that we do not run it on every push to a feature branch — only +# when there is an open PR aimed at `development`/`main`, or after a +# merge into `development`. The unit jobs (`go-unit.yaml`, +# `ui-test.yaml`) keep guarding fast feedback on every push. + +on: + pull_request: + branches: + - development + - main + paths: + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'ui/core/**' + - 'integration/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/integration.yaml' + - '!**/*.md' + push: + branches: + - development + paths: + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'ui/core/**' + - 'integration/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/integration.yaml' + - '!**/*.md' + +jobs: + integration: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.work + cache: true + + - name: Run integration suite + # `make integration` precleans leftover docker-compose state and + # then runs every test under integration/ serially (-p=1 + # -parallel=1, 15-minute per-test timeout). Testcontainers + # reaches the host's docker daemon via the socket Gitea exposes + # to the runner; the workflow inherits the same access the + # runner has. + run: make -C integration integration diff --git a/.gitea/workflows/prod-build.yaml b/.gitea/workflows/prod-build.yaml new file mode 100644 index 0000000..8018625 --- /dev/null +++ b/.gitea/workflows/prod-build.yaml @@ -0,0 +1,116 @@ +name: prod-build + +# Builds the production-grade Docker images and the UI bundle on every +# merge into `main`, then saves the artifacts so a future +# `deploy-prod.yaml` run can ship them to the production host. This +# workflow does not deploy anything by itself — production rollout is +# strictly manual (workflow_dispatch on `deploy-prod.yaml`). + +on: + push: + branches: + - main + paths: + - 'backend/**' + - 'gateway/**' + - 'game/**' + - 'pkg/**' + - 'ui/**' + - 'go.work' + - 'go.work.sum' + - '.gitea/workflows/prod-build.yaml' + - '!**/*.md' + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.work + cache: true + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.0.7 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: ui/pnpm-lock.yaml + + - name: Resolve image tag + id: tag + run: | + short_sha=$(git rev-parse --short=12 HEAD) + echo "tag=commit-${short_sha}" >>"$GITHUB_OUTPUT" + + - name: Build backend image + run: | + docker build \ + -t "galaxy/backend:${{ steps.tag.outputs.tag }}" \ + -f backend/Dockerfile \ + . + + - name: Build gateway image + run: | + docker build \ + -t "galaxy/gateway:${{ steps.tag.outputs.tag }}" \ + -f gateway/Dockerfile \ + . + + - name: Build engine image + run: | + docker build \ + -t "galaxy/game-engine:${{ steps.tag.outputs.tag }}" \ + -f game/Dockerfile \ + . + + - name: Install UI dependencies + working-directory: ui + run: pnpm install --frozen-lockfile + + - name: Build UI bundle + working-directory: ui/frontend + env: + VITE_GATEWAY_BASE_URL: https://api.galaxy.com + run: | + # Production response-signing public key is not in the repo + # yet (the dev key in `tools/local-dev/keys/` is for dev + # only). When real prod keys exist, source them from a Gitea + # Actions secret and set VITE_GATEWAY_RESPONSE_PUBLIC_KEY + # here. Until then the prod bundle compiles with the dev + # key as a placeholder so the artifact exists. + export VITE_GATEWAY_RESPONSE_PUBLIC_KEY="$(grep -E '^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=' .env.development | cut -d= -f2)" + pnpm build + + - name: Save images as artifact bundles + run: | + mkdir -p artifacts + docker save "galaxy/backend:${{ steps.tag.outputs.tag }}" \ + | gzip >"artifacts/backend-${{ steps.tag.outputs.tag }}.tar.gz" + docker save "galaxy/gateway:${{ steps.tag.outputs.tag }}" \ + | gzip >"artifacts/gateway-${{ steps.tag.outputs.tag }}.tar.gz" + docker save "galaxy/game-engine:${{ steps.tag.outputs.tag }}" \ + | gzip >"artifacts/game-engine-${{ steps.tag.outputs.tag }}.tar.gz" + tar -C ui/frontend -czf \ + "artifacts/ui-dist-${{ steps.tag.outputs.tag }}.tar.gz" build + + - name: Upload images + uses: actions/upload-artifact@v4 + with: + name: galaxy-images-${{ steps.tag.outputs.tag }} + path: artifacts/*.tar.gz + retention-days: 30 diff --git a/.gitea/workflows/ui-release.yaml b/.gitea/workflows/ui-release.yaml deleted file mode 100644 index f772f86..0000000 --- a/.gitea/workflows/ui-release.yaml +++ /dev/null @@ -1,148 +0,0 @@ -name: ui-release - -# Tier 2 (release) workflow. Runs on tag push. -# -# Currently mirrors the Tier 1 step set. Visual regression baseline -# checks and the macOS-runner iOS smoke job are landed in later phases -# of ui/PLAN.md and live as commented sections at the end of this file -# until those phases ship. - -on: - push: - tags: - - 'v*' - -jobs: - test: - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.work - cache: true - - - name: Run Go tests - # client/ is the deprecated Fyne client; excluded from CI per - # ui/PLAN.md §74. -count=1 disables Go's test cache so a green - # run never depends on a previous runner's cached state. The - # backend suite is run with -p 1 because most backend packages - # spawn their own Postgres testcontainer, and parallel - # Postgres bootstraps starve each other on a constrained - # runner. pkg modules are listed one by one because ./pkg/... - # does not recurse across the independent go.work modules - # under pkg/. - run: | - go test -count=1 -p 1 ./backend/... - go test -count=1 \ - ./gateway/... \ - ./game/... \ - ./ui/core/... \ - ./pkg/calc/... \ - ./pkg/connector/... \ - ./pkg/cronutil/... \ - ./pkg/error/... \ - ./pkg/geoip/... \ - ./pkg/model/... \ - ./pkg/postgres/... \ - ./pkg/redisconn/... \ - ./pkg/schema/... \ - ./pkg/storage/... \ - ./pkg/transcoder/... \ - ./pkg/util/... - - - name: Set up pnpm - uses: pnpm/action-setup@v4 - with: - version: 11.0.7 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - cache-dependency-path: ui/pnpm-lock.yaml - - - name: Install npm dependencies - working-directory: ui - run: pnpm install --frozen-lockfile - - - name: Install Playwright browsers - working-directory: ui/frontend - run: pnpm exec playwright install --with-deps - - - name: Run Vitest - working-directory: ui/frontend - run: pnpm test - - - name: Run Playwright - working-directory: ui/frontend - run: pnpm exec playwright test - - - name: Upload Playwright report on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: playwright-report - path: ui/frontend/playwright-report/ - retention-days: 14 - - - name: Upload Playwright traces on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: playwright-traces - path: ui/frontend/test-results/ - retention-days: 14 - -# visual-regression: enabled in Phase 33 of ui/PLAN.md, once the PWA -# shell and service worker land and a snapshot baseline is committed -# under ui/frontend/tests/__snapshots__/. -# -# visual-regression: -# runs-on: ubuntu-latest -# needs: test -# steps: -# - uses: actions/checkout@v4 -# - uses: pnpm/action-setup@v4 -# with: { version: 11.0.7 } -# - uses: actions/setup-node@v4 -# with: -# node-version: 22 -# cache: pnpm -# cache-dependency-path: ui/pnpm-lock.yaml -# - working-directory: ui -# run: pnpm install --frozen-lockfile -# - working-directory: ui/frontend -# run: pnpm exec playwright install --with-deps -# - working-directory: ui/frontend -# run: pnpm exec playwright test --grep @visual - -# ios-smoke: enabled in Phase 32 of ui/PLAN.md, once the Capacitor -# wrapper lands. Runs a Capacitor + Appium smoke against an iOS -# simulator on a macOS runner. -# -# ios-smoke: -# runs-on: macos-13 -# needs: test -# steps: -# - uses: actions/checkout@v4 -# - uses: pnpm/action-setup@v4 -# with: { version: 11.0.7 } -# - uses: actions/setup-node@v4 -# with: -# node-version: 22 -# cache: pnpm -# cache-dependency-path: ui/pnpm-lock.yaml -# - working-directory: ui -# run: pnpm install --frozen-lockfile -# - working-directory: ui/mobile -# run: pnpm exec cap sync ios && pnpm exec appium-smoke ios diff --git a/.gitea/workflows/ui-test.yaml b/.gitea/workflows/ui-test.yaml index b48dc6d..6c923aa 100644 --- a/.gitea/workflows/ui-test.yaml +++ b/.gitea/workflows/ui-test.yaml @@ -1,38 +1,18 @@ name: ui-test -# Tier 1 (per-PR) workflow. Runs Vitest + Playwright for the UI client and -# the monorepo Go service tests (everything except the integration suite, -# which lives behind `make -C integration integration` and needs a Docker -# daemon set up for testcontainers). -# -# The path filter is intentionally broad until a dedicated go-test -# workflow is introduced; this is the only CI gate today. +# UI-side unit and end-to-end tests (Vitest + Playwright). The Go side +# of the workspace is tested in `go-unit.yaml`. Both workflows can run +# in parallel for a push that touches Go and UI together. on: push: paths: - 'ui/**' - - 'backend/**' - - 'gateway/**' - - 'game/**' - - 'pkg/**' - - 'go.work' - - 'go.work.sum' - '.gitea/workflows/ui-test.yaml' - # Skip docs-only commits. Negation removes pure markdown changes; - # mixed commits (code + .md) still match a positive pattern above - # and trigger the workflow. Image and other binary asset paths - # are already outside the positive list. - '!**/*.md' pull_request: paths: - 'ui/**' - - 'backend/**' - - 'gateway/**' - - 'game/**' - - 'pkg/**' - - 'go.work' - - 'go.work.sum' - '.gitea/workflows/ui-test.yaml' - '!**/*.md' @@ -48,41 +28,6 @@ jobs: with: submodules: recursive - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.work - cache: true - - - name: Run Go tests - # client/ is the deprecated Fyne client; excluded from CI per - # ui/PLAN.md §74. -count=1 disables Go's test cache so a green - # run never depends on a previous runner's cached state. The - # backend suite is run with -p 1 because most backend packages - # spawn their own Postgres testcontainer, and parallel - # Postgres bootstraps starve each other on a constrained - # runner. pkg modules are listed one by one because ./pkg/... - # does not recurse across the independent go.work modules - # under pkg/. - run: | - go test -count=1 -p 1 ./backend/... - go test -count=1 \ - ./gateway/... \ - ./game/... \ - ./ui/core/... \ - ./pkg/calc/... \ - ./pkg/connector/... \ - ./pkg/cronutil/... \ - ./pkg/error/... \ - ./pkg/geoip/... \ - ./pkg/model/... \ - ./pkg/postgres/... \ - ./pkg/redisconn/... \ - ./pkg/schema/... \ - ./pkg/storage/... \ - ./pkg/transcoder/... \ - ./pkg/util/... - - name: Set up pnpm uses: pnpm/action-setup@v4 with: