chore(ci): tidy CI/dev infra — drop local-ci, lift migration rule #13
@@ -104,6 +104,24 @@ jobs:
|
|||||||
-v "${{ gitea.workspace }}/ui/frontend/build:/src:ro" \
|
-v "${{ gitea.workspace }}/ui/frontend/build:/src:ro" \
|
||||||
alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/'
|
alpine sh -c 'rm -rf /dst/* /dst/.??* 2>/dev/null; cp -a /src/. /dst/'
|
||||||
|
|
||||||
|
- name: Reap stray dev-deploy containers
|
||||||
|
run: |
|
||||||
|
# Remove any non-running compose-managed containers from
|
||||||
|
# earlier deploys before `compose up`. Filter by the stack
|
||||||
|
# label so we never touch unrelated workloads on the same
|
||||||
|
# daemon. Running containers (incl. engine instances backend
|
||||||
|
# spawned itself with the same label) are left intact —
|
||||||
|
# those are reattached by the backend reconciler on boot.
|
||||||
|
ids=$(docker ps -aq \
|
||||||
|
--filter "label=galaxy.stack=dev-deploy" \
|
||||||
|
--filter "status=exited" \
|
||||||
|
--filter "status=created" \
|
||||||
|
--filter "status=dead")
|
||||||
|
if [ -n "$ids" ]; then
|
||||||
|
echo "reaping: $ids"
|
||||||
|
docker rm -f $ids
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Bring up the stack
|
- name: Bring up the stack
|
||||||
working-directory: tools/dev-deploy
|
working-directory: tools/dev-deploy
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Branches:
|
|||||||
it auto-deploys to the dev environment via `dev-deploy.yaml`
|
it auto-deploys to the dev environment via `dev-deploy.yaml`
|
||||||
(reachable at `https://www.galaxy.lan` / `https://api.galaxy.lan`).
|
(reachable at `https://www.galaxy.lan` / `https://api.galaxy.lan`).
|
||||||
- `feature/*` — short-lived branches off `development`. Merged back
|
- `feature/*` — short-lived branches off `development`. Merged back
|
||||||
via PR; only then do they reach the dev environment.
|
via PR; only then do they reach the dev environment automatically.
|
||||||
|
|
||||||
Workflows in `.gitea/workflows/`:
|
Workflows in `.gitea/workflows/`:
|
||||||
|
|
||||||
@@ -55,10 +55,24 @@ Workflows in `.gitea/workflows/`:
|
|||||||
| `go-unit.yaml` | push + PR matching Go paths | Fast Go unit tests. |
|
| `go-unit.yaml` | push + PR matching Go paths | Fast Go unit tests. |
|
||||||
| `ui-test.yaml` | push + PR matching `ui/**` | Vitest + Playwright. |
|
| `ui-test.yaml` | push + PR matching `ui/**` | Vitest + Playwright. |
|
||||||
| `integration.yaml` | PR to `development`/`main`; push to `development` | testcontainers integration suite. |
|
| `integration.yaml` | PR to `development`/`main`; push to `development` | testcontainers integration suite. |
|
||||||
| `dev-deploy.yaml` | push to `development` | Build images + (re)deploy to `tools/dev-deploy/`. |
|
| `dev-deploy.yaml` | push to `development`; `workflow_dispatch` on any ref | Build images + (re)deploy to `tools/dev-deploy/`. |
|
||||||
| `prod-build.yaml` | push to `main` | Build prod images and `docker save` into artifacts. |
|
| `prod-build.yaml` | push to `main` | Build prod images and `docker save` into artifacts. |
|
||||||
| `deploy-prod.yaml` | `workflow_dispatch` | Manual rollout (placeholder until prod host exists). |
|
| `deploy-prod.yaml` | `workflow_dispatch` | Manual rollout (placeholder until prod host exists). |
|
||||||
|
|
||||||
|
### Deployment cadence
|
||||||
|
|
||||||
|
The long-lived dev environment (`tools/dev-deploy/`) is single-tenant:
|
||||||
|
one live deployment, redeployed on every merge into `development`.
|
||||||
|
While a PR is open the dev environment stays on whatever was last
|
||||||
|
merged — pushes to `feature/*` only fire the test workflows
|
||||||
|
(`go-unit`, `ui-test`, `integration`), not `dev-deploy.yaml`.
|
||||||
|
|
||||||
|
To preview an unmerged feature branch on the shared dev environment,
|
||||||
|
trigger `dev-deploy.yaml` manually from the Gitea UI
|
||||||
|
(**Actions → Deploy · Dev → Run workflow**) and pick the feature ref.
|
||||||
|
The deploy is idempotent: the next merge into `development` simply
|
||||||
|
overwrites whatever the manual dispatch left behind.
|
||||||
|
|
||||||
## Per-stage CI gate
|
## Per-stage CI gate
|
||||||
|
|
||||||
Every completed stage from any `PLAN.md` (per-service or `ui/PLAN.md`)
|
Every completed stage from any `PLAN.md` (per-service or `ui/PLAN.md`)
|
||||||
@@ -72,10 +86,6 @@ short version:
|
|||||||
4. Only after every workflow that fired is `success` may the stage be
|
4. Only after every workflow that fired is `success` may the stage be
|
||||||
marked done in the corresponding `PLAN.md`.
|
marked done in the corresponding `PLAN.md`.
|
||||||
|
|
||||||
`tools/local-ci/` is now an opt-in fallback for testing workflow
|
|
||||||
changes without `gitea.lan` (offline iterations, runner-isolation
|
|
||||||
debugging). It is no longer required for the per-stage gate.
|
|
||||||
|
|
||||||
## Decisions during stage implementation
|
## Decisions during stage implementation
|
||||||
|
|
||||||
Stages from `PLAN.md` produce decisions. Those decisions never live in a
|
Stages from `PLAN.md` produce decisions. Those decisions never live in a
|
||||||
@@ -102,18 +112,22 @@ The existing codebase of `galaxy/<service>` may be modified or extended when a
|
|||||||
plan stage requires it. All such changes must be covered by new or updated tests
|
plan stage requires it. All such changes must be covered by new or updated tests
|
||||||
and reflected in documentation when they affect documented behavior.
|
and reflected in documentation when they affect documented behavior.
|
||||||
|
|
||||||
## Pre-production migration rule
|
## Migrations
|
||||||
|
|
||||||
The platform is not yet in production. Schema changes for `backend` go
|
Schema changes for `backend` go into a new `0000N_*.sql` file under
|
||||||
into the existing `backend/internal/postgres/migrations/00001_init.sql`
|
`backend/internal/postgres/migrations/` with a monotonically increasing
|
||||||
file rather than into new `00002_*`-prefixed files. Local databases and
|
prefix. `00001_init.sql` is the historical baseline and stays
|
||||||
integration test harnesses are recreated from scratch on every pull.
|
immutable; every subsequent change is its own additive migration with
|
||||||
|
matching Up/Down sides. `pressly/goose/v3` (embedded into the backend
|
||||||
|
binary) applies pending migrations on startup, so the long-lived dev
|
||||||
|
environment picks up schema deltas without a manual reset.
|
||||||
|
|
||||||
**This rule is removed before the first production deployment.** From
|
Before the first production deployment the migration chain may be
|
||||||
that point on every schema change becomes a new migration file with a
|
squashed back into a single fresh `00001_init.sql` for a clean slate;
|
||||||
monotonically increasing prefix, and `00001_init.sql` becomes immutable
|
plan that work as an explicit task when it lands. See
|
||||||
history. See `backend/internal/postgres/migrations/README.md` for
|
`backend/internal/postgres/migrations/README.md` for the local
|
||||||
details.
|
authoring conventions (file naming, transactional vs. non-transactional
|
||||||
|
sections, backward-compatible deletes, rollback expectations).
|
||||||
|
|
||||||
## Documentation discipline
|
## Documentation discipline
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -129,6 +129,7 @@ fast.
|
|||||||
| `BACKEND_RUNTIME_CONTAINER_PIDS_LIMIT` | no | `256` | Engine container `--pids-limit`. |
|
| `BACKEND_RUNTIME_CONTAINER_PIDS_LIMIT` | no | `256` | Engine container `--pids-limit`. |
|
||||||
| `BACKEND_RUNTIME_CONTAINER_STATE_MOUNT` | no | `/var/lib/galaxy-game` | Absolute in-container path for the per-game state bind mount. |
|
| `BACKEND_RUNTIME_CONTAINER_STATE_MOUNT` | no | `/var/lib/galaxy-game` | Absolute in-container path for the per-game state bind mount. |
|
||||||
| `BACKEND_RUNTIME_STOP_GRACE_PERIOD` | no | `10s` | SIGTERM-to-SIGKILL grace period for engine container stop. |
|
| `BACKEND_RUNTIME_STOP_GRACE_PERIOD` | no | `10s` | SIGTERM-to-SIGKILL grace period for engine container stop. |
|
||||||
|
| `BACKEND_STACK_LABEL` | no | — | Optional value stamped as `galaxy.stack=<value>` on every engine container backend spawns. Lets host-side tooling (Makefile / CI) scope cleanup to one dev stack. Empty → label is not applied. |
|
||||||
| `BACKEND_NOTIFICATION_ADMIN_EMAIL` | no | — | Recipient address for admin-channel notifications (`runtime.*` kinds). When empty, admin-channel routes are recorded as `skipped` and the catalog is partially silenced. |
|
| `BACKEND_NOTIFICATION_ADMIN_EMAIL` | no | — | Recipient address for admin-channel notifications (`runtime.*` kinds). When empty, admin-channel routes are recorded as `skipped` and the catalog is partially silenced. |
|
||||||
| `BACKEND_NOTIFICATION_WORKER_INTERVAL` | no | `5s` | Notification route worker scan interval. |
|
| `BACKEND_NOTIFICATION_WORKER_INTERVAL` | no | `5s` | Notification route worker scan interval. |
|
||||||
| `BACKEND_NOTIFICATION_MAX_ATTEMPTS` | no | `8` | Notification route delivery attempts before dead-lettering. |
|
| `BACKEND_NOTIFICATION_MAX_ATTEMPTS` | no | `8` | Notification route delivery attempts before dead-lettering. |
|
||||||
@@ -153,10 +154,10 @@ seeded `admin_accounts` ahead of time.
|
|||||||
before the HTTP listener opens. The startup path also issues a
|
before the HTTP listener opens. The startup path also issues a
|
||||||
`CREATE SCHEMA IF NOT EXISTS backend` so a fresh database does not
|
`CREATE SCHEMA IF NOT EXISTS backend` so a fresh database does not
|
||||||
trip goose's bookkeeping table on the first migration.
|
trip goose's bookkeeping table on the first migration.
|
||||||
- Pre-production uses one migration file (`00001_init.sql`) covering
|
- Migrations are sequence-numbered (`0000N_*.sql`) and applied
|
||||||
every backend domain (auth, user, admin, lobby, runtime, mail,
|
additively. `00001_init.sql` is the historical baseline; every
|
||||||
notification, geo). Future migrations are sequence-numbered and
|
schema change after it is a new file with a higher prefix. See
|
||||||
additive.
|
`internal/postgres/migrations/README.md` for the authoring rules.
|
||||||
- Queries are written through `go-jet/jet/v2`. The generated code is in
|
- Queries are written through `go-jet/jet/v2`. The generated code is in
|
||||||
`internal/postgres/jet/backend/` and is committed; `internal/postgres/jet/jet.go`
|
`internal/postgres/jet/backend/` and is committed; `internal/postgres/jet/jet.go`
|
||||||
carries package metadata that survives regeneration.
|
carries package metadata that survives regeneration.
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ test stack. The list mirrors the steady-state behaviour documented in
|
|||||||
## Migrations
|
## Migrations
|
||||||
|
|
||||||
`pressly/goose/v3` applies embedded migrations from
|
`pressly/goose/v3` applies embedded migrations from
|
||||||
`internal/postgres/migrations/`. The pre-production set ships as
|
`internal/postgres/migrations/`. Migrations are additive,
|
||||||
`00001_init.sql` plus additive numbered files. Backend always runs
|
sequence-numbered files (`00001_init.sql` is the baseline). Backend
|
||||||
`CREATE SCHEMA IF NOT EXISTS backend` before goose so a fresh database
|
always runs `CREATE SCHEMA IF NOT EXISTS backend` before goose so a
|
||||||
does not trip the bookkeeping table on the first migration.
|
fresh database does not trip the bookkeeping table on the first
|
||||||
|
migration.
|
||||||
|
|
||||||
`internal/postgres/migrations_test.go` asserts that the migration
|
`internal/postgres/migrations_test.go` asserts that the migration
|
||||||
produces the expected table set; adding a table without updating the
|
produces the expected table set; adding a table without updating the
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const (
|
|||||||
envRuntimeContainerPIDsLimit = "BACKEND_RUNTIME_CONTAINER_PIDS_LIMIT"
|
envRuntimeContainerPIDsLimit = "BACKEND_RUNTIME_CONTAINER_PIDS_LIMIT"
|
||||||
envRuntimeContainerStateMount = "BACKEND_RUNTIME_CONTAINER_STATE_MOUNT"
|
envRuntimeContainerStateMount = "BACKEND_RUNTIME_CONTAINER_STATE_MOUNT"
|
||||||
envRuntimeStopGracePeriod = "BACKEND_RUNTIME_STOP_GRACE_PERIOD"
|
envRuntimeStopGracePeriod = "BACKEND_RUNTIME_STOP_GRACE_PERIOD"
|
||||||
|
envRuntimeStackLabel = "BACKEND_STACK_LABEL"
|
||||||
|
|
||||||
envNotificationAdminEmail = "BACKEND_NOTIFICATION_ADMIN_EMAIL"
|
envNotificationAdminEmail = "BACKEND_NOTIFICATION_ADMIN_EMAIL"
|
||||||
envNotificationWorkerInterval = "BACKEND_NOTIFICATION_WORKER_INTERVAL"
|
envNotificationWorkerInterval = "BACKEND_NOTIFICATION_WORKER_INTERVAL"
|
||||||
@@ -409,6 +410,14 @@ type RuntimeConfig struct {
|
|||||||
// StopGracePeriod is the docker stop SIGTERM-to-SIGKILL grace period
|
// StopGracePeriod is the docker stop SIGTERM-to-SIGKILL grace period
|
||||||
// applied during stop / cancel / restart / patch.
|
// applied during stop / cancel / restart / patch.
|
||||||
StopGracePeriod time.Duration
|
StopGracePeriod time.Duration
|
||||||
|
|
||||||
|
// StackLabel is the optional value backend stamps as
|
||||||
|
// `galaxy.stack=<value>` on every engine container it spawns. It
|
||||||
|
// lets host-side tooling (Makefile, CI workflows) scope cleanup
|
||||||
|
// operations to a single dev stack without touching unrelated
|
||||||
|
// workloads on the same Docker daemon. When empty, the label is
|
||||||
|
// not applied.
|
||||||
|
StackLabel string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiplomailConfig bounds the diplomatic-mail subsystem. Both limits
|
// DiplomailConfig bounds the diplomatic-mail subsystem. Both limits
|
||||||
@@ -705,6 +714,7 @@ func LoadFromEnv() (Config, error) {
|
|||||||
if cfg.Runtime.StopGracePeriod, err = loadDuration(envRuntimeStopGracePeriod, cfg.Runtime.StopGracePeriod); err != nil {
|
if cfg.Runtime.StopGracePeriod, err = loadDuration(envRuntimeStopGracePeriod, cfg.Runtime.StopGracePeriod); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
cfg.Runtime.StackLabel = strings.TrimSpace(loadString(envRuntimeStackLabel, cfg.Runtime.StackLabel))
|
||||||
|
|
||||||
cfg.Notification.AdminEmail = loadString(envNotificationAdminEmail, cfg.Notification.AdminEmail)
|
cfg.Notification.AdminEmail = loadString(envNotificationAdminEmail, cfg.Notification.AdminEmail)
|
||||||
if cfg.Notification.WorkerInterval, err = loadDuration(envNotificationWorkerInterval, cfg.Notification.WorkerInterval); err != nil {
|
if cfg.Notification.WorkerInterval, err = loadDuration(envNotificationWorkerInterval, cfg.Notification.WorkerInterval); err != nil {
|
||||||
|
|||||||
@@ -1,26 +1,46 @@
|
|||||||
# Backend migrations
|
# Backend migrations
|
||||||
|
|
||||||
Goose migrations embedded into the backend binary by `embed.go`. Applied
|
Goose (`pressly/goose/v3`) migrations embedded into the backend binary
|
||||||
at startup before any listener opens (see `internal/postgres`).
|
by `embed.go`. Applied at startup before any listener opens — see
|
||||||
|
`internal/postgres`.
|
||||||
|
|
||||||
## Pre-production single-file rule
|
## Authoring conventions
|
||||||
|
|
||||||
**While the platform is not yet in production, every schema change goes
|
- Each schema change is a new file with a monotonically increasing
|
||||||
into the existing `00001_init.sql` file** rather than a new
|
numeric prefix and a snake-case slug:
|
||||||
`00002_*`-prefixed file. The intent is to keep the schema in one
|
`0000N_short_description.sql`. Reuse of a prefix is forbidden once
|
||||||
canonical place so reviewers and developers do not have to reconstruct
|
the file is merged.
|
||||||
the latest shape from a chain of incremental migrations.
|
- `00001_init.sql` is the historical baseline. Treat it as immutable
|
||||||
|
history; do not edit it to land new schema. Squashing the chain back
|
||||||
|
into a fresh `00001` is reserved for the explicit pre-production
|
||||||
|
cut-over.
|
||||||
|
- Every file MUST contain both an `-- +goose Up` and `-- +goose Down`
|
||||||
|
section, even if Down is a single `DROP …` for the same artefacts.
|
||||||
|
Down migrations are exercised by the schema test and serve as the
|
||||||
|
documented rollback path.
|
||||||
|
- Destructive changes (dropping columns/tables, renaming with data
|
||||||
|
loss) MUST be split into at least two migrations so the chain stays
|
||||||
|
rollable forward and backward without coordinated code+schema
|
||||||
|
windows:
|
||||||
|
1. add the new shape, dual-write the data, leave the old shape in
|
||||||
|
place;
|
||||||
|
2. once all readers have switched, drop the old shape in a follow-up
|
||||||
|
migration.
|
||||||
|
- Migrations are applied automatically on backend startup, so a fresh
|
||||||
|
push to `development` plus the `dev-deploy.yaml` workflow brings the
|
||||||
|
long-lived dev database up to head without manual intervention.
|
||||||
|
`make -C tools/dev-deploy clean-data` is only needed when a developer
|
||||||
|
deliberately wants a fresh database.
|
||||||
|
- The integration harness (`backend/internal/postgres/migrations_test.go`)
|
||||||
|
spins up a disposable Postgres per run and asserts the final table
|
||||||
|
set. When a migration adds or removes tables, update the expected
|
||||||
|
list in the same patch.
|
||||||
|
|
||||||
Operationally this means that pulling a branch with schema changes
|
## Pre-production squash
|
||||||
requires a fresh database — the only consumer today is local development
|
|
||||||
and integration tests, both of which spin up disposable Postgres
|
|
||||||
instances.
|
|
||||||
|
|
||||||
> **Remove this rule before the first production deployment.** From
|
The chain may be squashed back into one clean `00001_init.sql` before
|
||||||
> that point on every schema change must be a new migration file with a
|
the first production deployment. That is a deliberate, one-time
|
||||||
> monotonically increasing prefix, and `00001_init.sql` becomes
|
operation; until then, additive numbered files are the rule. After the
|
||||||
> immutable history.
|
squash this file gets a short note that `00001_init.sql` represents
|
||||||
|
the production baseline and the policy above continues to apply for
|
||||||
If you need to make a change, edit `00001_init.sql` directly. Down
|
every later migration.
|
||||||
migrations should still be kept in sync (they live at the bottom of the
|
|
||||||
file — currently a single `DROP SCHEMA backend CASCADE`).
|
|
||||||
|
|||||||
@@ -537,10 +537,7 @@ func (s *Service) runStart(ctx context.Context, op OperationLog) error {
|
|||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
"GAME_STATE_PATH": statePath,
|
"GAME_STATE_PATH": statePath,
|
||||||
},
|
},
|
||||||
Labels: map[string]string{
|
Labels: s.engineLabels(gameID.String(), version.Version),
|
||||||
"galaxy.game_id": gameID.String(),
|
|
||||||
"galaxy.engine_version": version.Version,
|
|
||||||
},
|
|
||||||
BindMounts: []dockerclient.BindMount{
|
BindMounts: []dockerclient.BindMount{
|
||||||
{
|
{
|
||||||
HostPath: hostStatePath,
|
HostPath: hostStatePath,
|
||||||
@@ -735,10 +732,7 @@ func (s *Service) runPatch(ctx context.Context, op OperationLog, target EngineVe
|
|||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
"GAME_STATE_PATH": statePath,
|
"GAME_STATE_PATH": statePath,
|
||||||
},
|
},
|
||||||
Labels: map[string]string{
|
Labels: s.engineLabels(op.GameID.String(), target.Version),
|
||||||
"galaxy.game_id": op.GameID.String(),
|
|
||||||
"galaxy.engine_version": target.Version,
|
|
||||||
},
|
|
||||||
BindMounts: []dockerclient.BindMount{
|
BindMounts: []dockerclient.BindMount{
|
||||||
{HostPath: hostStatePath, MountPath: s.deps.Config.ContainerStateMount},
|
{HostPath: hostStatePath, MountPath: s.deps.Config.ContainerStateMount},
|
||||||
},
|
},
|
||||||
@@ -938,6 +932,30 @@ func (s *Service) upsertRuntimeRecord(ctx context.Context, in runtimeRecordInser
|
|||||||
// containers attach to. Wired from cfg.Docker.Network through Deps.
|
// containers attach to. Wired from cfg.Docker.Network through Deps.
|
||||||
func (s *Service) dockerNetwork() string { return s.deps.DockerNetwork }
|
func (s *Service) dockerNetwork() string { return s.deps.DockerNetwork }
|
||||||
|
|
||||||
|
// engineLabels returns the label set stamped on every engine container
|
||||||
|
// spawned for gameID running engineVersion. The runtime adapter merges
|
||||||
|
// `dockerclient.ManagedLabel` separately; this helper covers the
|
||||||
|
// game-scoped labels plus an optional `galaxy.stack=<value>` from the
|
||||||
|
// runtime config so host-side tooling can scope cleanup by dev stack
|
||||||
|
// without touching unrelated workloads.
|
||||||
|
func (s *Service) engineLabels(gameID, engineVersion string) map[string]string {
|
||||||
|
return engineLabels(gameID, engineVersion, s.deps.Config.StackLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// engineLabels is the side-effect-free part of `(*Service).engineLabels`,
|
||||||
|
// exposed at package scope so unit tests can exercise the labelling
|
||||||
|
// rules without building a full Service.
|
||||||
|
func engineLabels(gameID, engineVersion, stackLabel string) map[string]string {
|
||||||
|
labels := map[string]string{
|
||||||
|
"galaxy.game_id": gameID,
|
||||||
|
"galaxy.engine_version": engineVersion,
|
||||||
|
}
|
||||||
|
if stackLabel != "" {
|
||||||
|
labels["galaxy.stack"] = stackLabel
|
||||||
|
}
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
// waitForEngineHealthz polls the engine `/healthz` endpoint until it
|
// waitForEngineHealthz polls the engine `/healthz` endpoint until it
|
||||||
// responds 2xx or until the timeout elapses. The Docker daemon
|
// responds 2xx or until the timeout elapses. The Docker daemon
|
||||||
// reports a container as `running` as soon as the entrypoint starts,
|
// reports a container as `running` as soon as the entrypoint starts,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package runtime
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEngineLabels(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
gameID string
|
||||||
|
version string
|
||||||
|
stackLabel string
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "stack label omitted when empty",
|
||||||
|
gameID: "11111111-1111-1111-1111-111111111111",
|
||||||
|
version: "0.1.0",
|
||||||
|
stackLabel: "",
|
||||||
|
want: map[string]string{
|
||||||
|
"galaxy.game_id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"galaxy.engine_version": "0.1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stack label included when set",
|
||||||
|
gameID: "22222222-2222-2222-2222-222222222222",
|
||||||
|
version: "0.2.3",
|
||||||
|
stackLabel: "dev-deploy",
|
||||||
|
want: map[string]string{
|
||||||
|
"galaxy.game_id": "22222222-2222-2222-2222-222222222222",
|
||||||
|
"galaxy.engine_version": "0.2.3",
|
||||||
|
"galaxy.stack": "dev-deploy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := engineLabels(tc.gameID, tc.version, tc.stackLabel)
|
||||||
|
if len(got) != len(tc.want) {
|
||||||
|
t.Fatalf("len(labels) = %d, want %d (got %v)", len(got), len(tc.want), got)
|
||||||
|
}
|
||||||
|
for k, v := range tc.want {
|
||||||
|
if got[k] != v {
|
||||||
|
t.Errorf("labels[%q] = %q, want %q", k, got[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-4
@@ -808,10 +808,17 @@ Workflows under `.gitea/workflows/`:
|
|||||||
| `go-unit.yaml` | push + PR matching Go paths | Fast Go unit tests. |
|
| `go-unit.yaml` | push + PR matching Go paths | Fast Go unit tests. |
|
||||||
| `ui-test.yaml` | push + PR matching `ui/**` | Vitest + Playwright. |
|
| `ui-test.yaml` | push + PR matching `ui/**` | Vitest + Playwright. |
|
||||||
| `integration.yaml` | PR to `development` / `main`; push to `development` | testcontainers integration suite. |
|
| `integration.yaml` | PR to `development` / `main`; push to `development` | testcontainers integration suite. |
|
||||||
| `dev-deploy.yaml` | push to `development` | Build images, seed UI volume, `compose up` against `tools/dev-deploy/`. |
|
| `dev-deploy.yaml` | push to `development`; `workflow_dispatch` on any ref | Build images, seed UI volume, `compose up` against `tools/dev-deploy/`. |
|
||||||
| `prod-build.yaml` | push to `main` | Build production images and persist `docker save` bundles as artifacts. |
|
| `prod-build.yaml` | push to `main` | Build production images and persist `docker save` bundles as artifacts. |
|
||||||
| `deploy-prod.yaml` | manual `workflow_dispatch` | Placeholder for the future SSH-based production rollout. |
|
| `deploy-prod.yaml` | manual `workflow_dispatch` | Placeholder for the future SSH-based production rollout. |
|
||||||
|
|
||||||
|
Deployment cadence: the dev environment is single-tenant. Pushes to
|
||||||
|
`feature/*` branches run only the test workflows; `dev-deploy.yaml`
|
||||||
|
does not auto-fire. To preview a feature branch on the shared dev
|
||||||
|
environment, trigger `dev-deploy.yaml` manually from the Gitea UI
|
||||||
|
against the desired ref. The deploy is idempotent — the next merge
|
||||||
|
into `development` overwrites the manually deployed state.
|
||||||
|
|
||||||
Environments:
|
Environments:
|
||||||
|
|
||||||
- **`tools/local-dev/`** — single-developer playground. Bound to
|
- **`tools/local-dev/`** — single-developer playground. Bound to
|
||||||
@@ -823,9 +830,37 @@ Environments:
|
|||||||
and are shipped to the production host via `docker save` →
|
and are shipped to the production host via `docker save` →
|
||||||
`ssh prod docker load` → `docker compose up -d`.
|
`ssh prod docker load` → `docker compose up -d`.
|
||||||
|
|
||||||
`tools/local-ci/` remains as an opt-in fallback runner for testing
|
### Container labels
|
||||||
workflow changes without `gitea.lan`. It is no longer part of the
|
|
||||||
per-stage CI gate; see `CLAUDE.md` for the gate definition.
|
Every Galaxy-managed Docker **container** carries an opinionated
|
||||||
|
label so that host-side tooling (Makefiles, CI workflows,
|
||||||
|
`preclean.sh`) can scope its operations to Galaxy-owned containers
|
||||||
|
and never touch unrelated workloads on the shared daemon.
|
||||||
|
|
||||||
|
| Label | Values | Set by | Used by |
|
||||||
|
|-------|--------|--------|---------|
|
||||||
|
| `galaxy.stack` | `local-dev`, `dev-deploy`, `integration` | `tools/{local-dev,dev-deploy}/docker-compose.yml` for compose-managed services; backend reads `BACKEND_STACK_LABEL` and stamps engines it spawns. | `tools/{local-dev,dev-deploy}/Makefile`, `.gitea/workflows/dev-deploy.yaml`. |
|
||||||
|
| `galaxy.backend` | `1` | `backend/internal/dockerclient` adapter on every engine container. | `integration/scripts/preclean.sh`. |
|
||||||
|
| `galaxy.game_id` | `<uuid>` | Backend on engine create. | Reconciler reattach loop. |
|
||||||
|
| `galaxy.engine_version` | `<semver>` | Backend on engine create. | Reconciler version checks. |
|
||||||
|
| `galaxy.test.kind` | `integration-image` | `integration/testenv/images.go` on local image builds. | `integration/scripts/preclean.sh` (filter for `docker rmi`). |
|
||||||
|
| `org.testcontainers` | `true` | `testcontainers-go` (automatic). | `integration/scripts/preclean.sh`. |
|
||||||
|
|
||||||
|
The contract: any Makefile target, CI step, or script that issues
|
||||||
|
`docker rm` / `docker rmi` / `docker network rm` MUST scope itself via
|
||||||
|
one of the labels above. Compose-managed resources are additionally
|
||||||
|
scoped by their compose project name (`galaxy-dev`, `galaxy-local-dev`),
|
||||||
|
which Compose enforces on `docker compose up/down`; the labels make the
|
||||||
|
contract explicit and survive hand-rolled cleanup commands as well.
|
||||||
|
|
||||||
|
**Scope deliberately limited to containers.** Labels are NOT stamped
|
||||||
|
on named volumes or user-defined networks. Adding labels there would
|
||||||
|
change the compose config-hash for the volume/network on every label
|
||||||
|
revision and force `docker compose up` to recreate them — which for a
|
||||||
|
postgres data volume means destroying the database, and for a shared
|
||||||
|
network can deadlock if any container is still attached. Containers
|
||||||
|
alone are sufficient for the cleanup contract; stateful resources stay
|
||||||
|
untouched by compose between deploys.
|
||||||
|
|
||||||
## 19. Deployment Topology (informational)
|
## 19. Deployment Topology (informational)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
REPO_ROOT := $(realpath $(CURDIR)/../..)
|
REPO_ROOT := $(realpath $(CURDIR)/../..)
|
||||||
ENGINE_IMAGE := galaxy-engine:dev
|
ENGINE_IMAGE := galaxy-engine:dev
|
||||||
|
STACK_LABEL := galaxy.stack=dev-deploy
|
||||||
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine
|
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine
|
||||||
# Game-state root lives under the invoking user's home by default so
|
# Game-state root lives under the invoking user's home by default so
|
||||||
# `make up` works without sudo. Override `GALAXY_DEV_GAME_STATE_DIR`
|
# `make up` works without sudo. Override `GALAXY_DEV_GAME_STATE_DIR`
|
||||||
@@ -93,12 +94,14 @@ psql:
|
|||||||
|
|
||||||
clean-data:
|
clean-data:
|
||||||
@echo "Stopping containers and engines, then wiping volumes + game-state…"
|
@echo "Stopping containers and engines, then wiping volumes + game-state…"
|
||||||
@ids=$$(docker ps -aq --filter label=$(ENGINE_LABEL)); \
|
$(COMPOSE) down -v
|
||||||
|
@ids=$$(docker ps -aq \
|
||||||
|
--filter "label=$(STACK_LABEL)" \
|
||||||
|
--filter "label=$(ENGINE_LABEL)"); \
|
||||||
if [ -n "$$ids" ]; then \
|
if [ -n "$$ids" ]; then \
|
||||||
echo "stopping engine containers…"; \
|
echo "stopping engine containers for $(STACK_LABEL)…"; \
|
||||||
docker rm -f $$ids >/dev/null; \
|
docker rm -f $$ids >/dev/null; \
|
||||||
fi
|
fi
|
||||||
$(COMPOSE) down -v
|
|
||||||
@if [ -d "$(GALAXY_DEV_GAME_STATE_DIR)" ]; then \
|
@if [ -d "$(GALAXY_DEV_GAME_STATE_DIR)" ]; then \
|
||||||
echo "wiping $(GALAXY_DEV_GAME_STATE_DIR)…"; \
|
echo "wiping $(GALAXY_DEV_GAME_STATE_DIR)…"; \
|
||||||
docker run --rm -v "$(GALAXY_DEV_GAME_STATE_DIR):/state" alpine sh -c 'rm -rf /state/*' 2>/dev/null \
|
docker run --rm -v "$(GALAXY_DEV_GAME_STATE_DIR):/state" alpine sh -c 'rm -rf /state/*' 2>/dev/null \
|
||||||
|
|||||||
@@ -135,17 +135,20 @@ exec galaxy-mailpit wget -qO- localhost:8025/messages` and similar.
|
|||||||
## Persistent state and schema changes
|
## Persistent state and schema changes
|
||||||
|
|
||||||
The dev Postgres volume `galaxy-dev-postgres-data` survives redeploys.
|
The dev Postgres volume `galaxy-dev-postgres-data` survives redeploys.
|
||||||
Until the pre-production migration rule is lifted, every
|
Schema deltas land as additive, sequence-numbered migration files
|
||||||
backward-incompatible change to `backend/internal/postgres/migrations/00001_init.sql`
|
(`backend/internal/postgres/migrations/0000N_*.sql`) and `pressly/goose`
|
||||||
needs a manual wipe before the next deploy succeeds:
|
applies them on backend startup without operator action.
|
||||||
|
|
||||||
|
Use `make -C tools/dev-deploy clean-data` only when you deliberately
|
||||||
|
want a fresh database (debugging schema drift, exercising the
|
||||||
|
bootstrap path from scratch, etc.):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make -C tools/dev-deploy clean-data
|
make -C tools/dev-deploy clean-data
|
||||||
make -C tools/dev-deploy up
|
make -C tools/dev-deploy up
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the same caveat as `tools/local-dev/`, just with a different
|
The same volume-persistence model applies to `tools/local-dev/`.
|
||||||
volume name.
|
|
||||||
|
|
||||||
## Make targets
|
## Make targets
|
||||||
|
|
||||||
@@ -183,13 +186,30 @@ See [`KNOWN-ISSUES.md`](KNOWN-ISSUES.md) for symptoms that surface
|
|||||||
in the long-lived dev environment but are not yet fixed (currently:
|
in the long-lived dev environment but are not yet fixed (currently:
|
||||||
the sandbox game flipping to `cancelled` after a redispatch).
|
the sandbox game flipping to `cancelled` after a redispatch).
|
||||||
|
|
||||||
|
## Deployment cadence
|
||||||
|
|
||||||
|
This environment is single-tenant: one live deployment, redeployed by
|
||||||
|
the `dev-deploy.yaml` workflow on every merge into `development`. PR
|
||||||
|
branches do not auto-deploy here — pushes to `feature/*` only run the
|
||||||
|
test workflows (`go-unit`, `ui-test`, `integration`).
|
||||||
|
|
||||||
|
To put a feature branch on the shared dev environment before its PR
|
||||||
|
merges (e.g. to validate a UI flow against the real Caddy edge), run
|
||||||
|
the workflow manually:
|
||||||
|
|
||||||
|
1. Push the branch (`git push gitea HEAD`).
|
||||||
|
2. Gitea UI → **Actions → Deploy · Dev → Run workflow**, pick the
|
||||||
|
feature ref.
|
||||||
|
|
||||||
|
The deploy is idempotent — when the PR later merges into
|
||||||
|
`development`, the regular push trigger fires the same packaging and
|
||||||
|
healthcheck steps, overwriting whatever the manual dispatch left
|
||||||
|
behind. There is no separate state to clean up between the two paths.
|
||||||
|
|
||||||
## Relationship to other infrastructure
|
## Relationship to other infrastructure
|
||||||
|
|
||||||
- `tools/local-dev/` — single-developer playground, host-port mapped,
|
- `tools/local-dev/` — single-developer playground, host-port mapped,
|
||||||
Vite dev server on the side. Recommended for active UI work.
|
Vite dev server on the side. Recommended for active UI work.
|
||||||
- `tools/local-ci/` — Gitea + act runner for **fallback** workflow
|
|
||||||
testing without `gitea.lan`. Optional, not part of the per-stage CI
|
|
||||||
gate anymore.
|
|
||||||
- `.gitea/workflows/dev-deploy.yaml` — the CI side of this stack:
|
- `.gitea/workflows/dev-deploy.yaml` — the CI side of this stack:
|
||||||
builds images, seeds the UI volume, runs `docker compose up -d` on
|
builds images, seeds the UI volume, runs `docker compose up -d` on
|
||||||
every merge into `development`. The Makefile in this directory is
|
every merge into `development`. The Makefile in this directory is
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ services:
|
|||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: galaxy-dev-postgres
|
container_name: galaxy-dev-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: dev-deploy
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: galaxy
|
POSTGRES_USER: galaxy
|
||||||
POSTGRES_PASSWORD: galaxy
|
POSTGRES_PASSWORD: galaxy
|
||||||
@@ -41,6 +43,8 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: galaxy-dev-redis
|
container_name: galaxy-dev-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: dev-deploy
|
||||||
command:
|
command:
|
||||||
- redis-server
|
- redis-server
|
||||||
- --requirepass
|
- --requirepass
|
||||||
@@ -62,6 +66,8 @@ services:
|
|||||||
image: axllent/mailpit:v1.21
|
image: axllent/mailpit:v1.21
|
||||||
container_name: galaxy-dev-mailpit
|
container_name: galaxy-dev-mailpit
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: dev-deploy
|
||||||
networks:
|
networks:
|
||||||
- galaxy-internal
|
- galaxy-internal
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -78,6 +84,8 @@ services:
|
|||||||
image: galaxy/backend:dev
|
image: galaxy/backend:dev
|
||||||
container_name: galaxy-dev-backend
|
container_name: galaxy-dev-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: dev-deploy
|
||||||
user: "0:0"
|
user: "0:0"
|
||||||
depends_on:
|
depends_on:
|
||||||
galaxy-postgres:
|
galaxy-postgres:
|
||||||
@@ -94,6 +102,7 @@ services:
|
|||||||
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.lan"
|
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.lan"
|
||||||
BACKEND_SMTP_TLS_MODE: none
|
BACKEND_SMTP_TLS_MODE: none
|
||||||
BACKEND_DOCKER_NETWORK: galaxy-dev-internal
|
BACKEND_DOCKER_NETWORK: galaxy-dev-internal
|
||||||
|
BACKEND_STACK_LABEL: dev-deploy
|
||||||
BACKEND_GAME_STATE_ROOT: ${GALAXY_DEV_GAME_STATE_DIR}
|
BACKEND_GAME_STATE_ROOT: ${GALAXY_DEV_GAME_STATE_DIR}
|
||||||
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
|
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
|
||||||
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.lan
|
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.lan
|
||||||
@@ -152,6 +161,8 @@ services:
|
|||||||
image: galaxy/gateway:dev
|
image: galaxy/gateway:dev
|
||||||
container_name: galaxy-dev-api
|
container_name: galaxy-dev-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: dev-deploy
|
||||||
depends_on:
|
depends_on:
|
||||||
galaxy-backend:
|
galaxy-backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -209,6 +220,8 @@ services:
|
|||||||
image: caddy:2.11.2-alpine
|
image: caddy:2.11.2-alpine
|
||||||
container_name: galaxy-dev-caddy
|
container_name: galaxy-dev-caddy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: dev-deploy
|
||||||
depends_on:
|
depends_on:
|
||||||
galaxy-api:
|
galaxy-api:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -229,6 +242,15 @@ networks:
|
|||||||
name: ${GALAXY_EDGE_NETWORK:-edge}
|
name: ${GALAXY_EDGE_NETWORK:-edge}
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
|
# Note: `galaxy.stack=dev-deploy` is intentionally stamped only on
|
||||||
|
# services (containers). Stamping it on networks or named volumes
|
||||||
|
# changes the compose config-hash for those resources, and on a
|
||||||
|
# subsequent `compose up` compose tries to recreate them — for the
|
||||||
|
# `galaxy-dev-postgres-data` volume that means destroying the
|
||||||
|
# database, and for `galaxy-dev-internal` it can deadlock if any
|
||||||
|
# container is still attached. Per-container labels are sufficient
|
||||||
|
# for the CI/cleanup contract; we filter containers, not volumes or
|
||||||
|
# networks.
|
||||||
volumes:
|
volumes:
|
||||||
galaxy-dev-postgres-data:
|
galaxy-dev-postgres-data:
|
||||||
name: galaxy-dev-postgres-data
|
name: galaxy-dev-postgres-data
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
.env
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
.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
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Local Gitea CI (fallback)
|
|
||||||
|
|
||||||
> **Status:** fallback / opt-in. The primary CI target is now
|
|
||||||
> `gitea.lan` with its host-mode `act_runner`. The per-stage CI gate
|
|
||||||
> closes against `gitea.lan`, not against this stack. Use this
|
|
||||||
> directory when you want to validate `.gitea/workflows/*` without
|
|
||||||
> reaching `gitea.lan` — for example, when iterating on a workflow
|
|
||||||
> file from a flight without LAN access — or when isolating a runner
|
|
||||||
> issue from production-shaped infrastructure.
|
|
||||||
|
|
||||||
Self-contained Gitea + Actions runner for verifying
|
|
||||||
`.gitea/workflows/*` honestly before pushing to `gitea.lan`. 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.
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# 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: ""
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Local-only override: this developer's host already runs another
|
|
||||||
# Gitea instance bound to 0.0.0.0:3000 and 0.0.0.0:2222, so the
|
|
||||||
# default port mappings in docker-compose.yml conflict. Remap the
|
|
||||||
# local-ci Gitea to 13000 (HTTP) and 12222 (SSH) on the host. The
|
|
||||||
# in-network ports stay 3000 / 22 — runners and workflow containers
|
|
||||||
# keep reaching Gitea by hostname through the compose network.
|
|
||||||
#
|
|
||||||
# This file is intentionally NOT committed to the repo; it captures
|
|
||||||
# per-host port allocation. Use `make -C tools/local-ci push` only
|
|
||||||
# after pointing the `local-gitea` git remote at the override port.
|
|
||||||
|
|
||||||
services:
|
|
||||||
gitea:
|
|
||||||
ports: !override
|
|
||||||
- "13000:3000"
|
|
||||||
- "12222:22"
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -5,9 +5,16 @@
|
|||||||
COMPOSE := docker compose
|
COMPOSE := docker compose
|
||||||
REPO_ROOT := $(realpath $(CURDIR)/../..)
|
REPO_ROOT := $(realpath $(CURDIR)/../..)
|
||||||
ENGINE_IMAGE := galaxy-engine:local-dev
|
ENGINE_IMAGE := galaxy-engine:local-dev
|
||||||
# Label set by the engine `Dockerfile` runtime stage; used to find
|
# Engine containers spawned by backend's runtime fall outside the
|
||||||
# engine containers spawned by backend's runtime that fall outside
|
# compose project. We identify them by two labels:
|
||||||
# `docker compose down`'s scope.
|
# STACK_LABEL — backend stamps this on every engine it spawns from
|
||||||
|
# this stack (see BACKEND_STACK_LABEL env in the
|
||||||
|
# compose file);
|
||||||
|
# ENGINE_LABEL — image-level OCI title baked into the engine
|
||||||
|
# Dockerfile.
|
||||||
|
# Both filters together select exactly this stack's engine containers
|
||||||
|
# and never compose-managed services or unrelated workloads.
|
||||||
|
STACK_LABEL := galaxy.stack=local-dev
|
||||||
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine
|
ENGINE_LABEL := org.opencontainers.image.title=galaxy-game-engine
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@@ -65,9 +72,11 @@ clean: stop-engines
|
|||||||
# cascade the game to `cancelled`. We only remove them as part of
|
# cascade the game to `cancelled`. We only remove them as part of
|
||||||
# `clean`, where the whole DB is wiped anyway.
|
# `clean`, where the whole DB is wiped anyway.
|
||||||
stop-engines:
|
stop-engines:
|
||||||
@ids=$$(docker ps -aq --filter label=$(ENGINE_LABEL)); \
|
@ids=$$(docker ps -aq \
|
||||||
|
--filter "label=$(STACK_LABEL)" \
|
||||||
|
--filter "label=$(ENGINE_LABEL)"); \
|
||||||
if [ -n "$$ids" ]; then \
|
if [ -n "$$ids" ]; then \
|
||||||
echo "stopping engine containers…"; \
|
echo "stopping engine containers for $(STACK_LABEL)…"; \
|
||||||
docker rm -f $$ids >/dev/null; \
|
docker rm -f $$ids >/dev/null; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -87,7 +96,9 @@ stop-engines:
|
|||||||
# cycles.
|
# cycles.
|
||||||
prune-broken-engines:
|
prune-broken-engines:
|
||||||
@ids=""; \
|
@ids=""; \
|
||||||
for cid in $$(docker ps -aq --filter label=$(ENGINE_LABEL) 2>/dev/null); do \
|
for cid in $$(docker ps -aq \
|
||||||
|
--filter "label=$(STACK_LABEL)" \
|
||||||
|
--filter "label=$(ENGINE_LABEL)" 2>/dev/null); do \
|
||||||
state=$$(docker inspect -f '{{.State.Status}}' $$cid 2>/dev/null); \
|
state=$$(docker inspect -f '{{.State.Status}}' $$cid 2>/dev/null); \
|
||||||
case "$$state" in \
|
case "$$state" in \
|
||||||
running|restarting) ;; \
|
running|restarting) ;; \
|
||||||
|
|||||||
+13
-12
@@ -15,10 +15,10 @@ This stack is **not** a CI gate (the per-stage CI gate now lives on
|
|||||||
the **long-lived dev environment** at
|
the **long-lived dev environment** at
|
||||||
[`tools/dev-deploy/`](../dev-deploy/README.md), which is redeployed on
|
[`tools/dev-deploy/`](../dev-deploy/README.md), which is redeployed on
|
||||||
every merge into `development` and is reachable as
|
every merge into `development` and is reachable as
|
||||||
`https://www.galaxy.lan` / `https://api.galaxy.lan`. The three stacks
|
`https://www.galaxy.lan` / `https://api.galaxy.lan`. The two stacks
|
||||||
(`tools/local-dev/`, `tools/dev-deploy/`, and the fallback
|
(`tools/local-dev/` and `tools/dev-deploy/`) coexist on the same host
|
||||||
`tools/local-ci/`) coexist on the same host because every name —
|
because every name — compose project, container, network, volume — is
|
||||||
compose project, container, network, volume — is distinct.
|
distinct.
|
||||||
|
|
||||||
## Bring it up
|
## Bring it up
|
||||||
|
|
||||||
@@ -203,8 +203,8 @@ make status docker compose ps
|
|||||||
images built on alpine (so `wget` is available for the compose
|
images built on alpine (so `wget` is available for the compose
|
||||||
healthchecks). The build stage mirrors `backend/Dockerfile` and
|
healthchecks). The build stage mirrors `backend/Dockerfile` and
|
||||||
`gateway/Dockerfile` exactly.
|
`gateway/Dockerfile` exactly.
|
||||||
- `Makefile` — wrapper over `docker compose` that keeps the muscle
|
- `Makefile` — wrapper over `docker compose` with thin targets for the
|
||||||
memory close to `tools/local-ci/`'s Makefile.
|
most common dev cycles.
|
||||||
- `.env` — committed defaults for the compose `${VAR:-}`
|
- `.env` — committed defaults for the compose `${VAR:-}`
|
||||||
expansions. Edit per-developer or override via your shell.
|
expansions. Edit per-developer or override via your shell.
|
||||||
- `keys/gateway-response.pem`, `keys/gateway-response.pub` — dev-only
|
- `keys/gateway-response.pem`, `keys/gateway-response.pub` — dev-only
|
||||||
@@ -290,12 +290,13 @@ make status docker compose ps
|
|||||||
|
|
||||||
## Relationship to other infrastructure
|
## Relationship to other infrastructure
|
||||||
|
|
||||||
- `tools/local-ci/` — Gitea + Actions runner, replays
|
- `tools/dev-deploy/` — long-lived dev environment redeployed on every
|
||||||
`.gitea/workflows/*` against a pushed branch. Different stack,
|
merge into `development`; reachable at `https://www.galaxy.lan` /
|
||||||
different purpose; coexists with local-dev on the same machine.
|
`https://api.galaxy.lan`. Distinct compose project, container names,
|
||||||
|
network and volumes.
|
||||||
- `integration/testenv/` — testcontainers harness used by
|
- `integration/testenv/` — testcontainers harness used by
|
||||||
`make -C integration integration`. Uses the same images
|
`make -C integration integration`. Uses the canonical
|
||||||
(`backend/Dockerfile`, `gateway/Dockerfile`) at production
|
`backend/Dockerfile` / `gateway/Dockerfile` at production defaults;
|
||||||
defaults; do not confuse with this local-dev stack, which carries
|
do not confuse with this local-dev stack, which carries
|
||||||
alpine-runtime images for ergonomics and the dev-mode auth
|
alpine-runtime images for ergonomics and the dev-mode auth
|
||||||
override.
|
override.
|
||||||
|
|||||||
@@ -19,11 +19,15 @@
|
|||||||
# can log in without touching Mailpit. Real codes still arrive in
|
# can log in without touching Mailpit. Real codes still arrive in
|
||||||
# Mailpit; both paths coexist.
|
# Mailpit; both paths coexist.
|
||||||
|
|
||||||
|
name: galaxy-local-dev
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: galaxy-local-dev-postgres
|
container_name: galaxy-local-dev-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: local-dev
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: galaxy
|
POSTGRES_USER: galaxy
|
||||||
POSTGRES_PASSWORD: galaxy
|
POSTGRES_PASSWORD: galaxy
|
||||||
@@ -45,6 +49,8 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: galaxy-local-dev-redis
|
container_name: galaxy-local-dev-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: local-dev
|
||||||
command:
|
command:
|
||||||
- redis-server
|
- redis-server
|
||||||
- --requirepass
|
- --requirepass
|
||||||
@@ -68,6 +74,8 @@ services:
|
|||||||
image: axllent/mailpit:v1.21
|
image: axllent/mailpit:v1.21
|
||||||
container_name: galaxy-local-dev-mailpit
|
container_name: galaxy-local-dev-mailpit
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: local-dev
|
||||||
ports:
|
ports:
|
||||||
- "${LOCAL_DEV_MAILPIT_PORT:-8025}:8025"
|
- "${LOCAL_DEV_MAILPIT_PORT:-8025}:8025"
|
||||||
networks:
|
networks:
|
||||||
@@ -86,6 +94,8 @@ services:
|
|||||||
image: galaxy/backend:local-dev
|
image: galaxy/backend:local-dev
|
||||||
container_name: galaxy-local-dev-backend
|
container_name: galaxy-local-dev-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: local-dev
|
||||||
user: "0:0"
|
user: "0:0"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -102,6 +112,7 @@ services:
|
|||||||
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.local"
|
BACKEND_SMTP_FROM: "galaxy-backend@galaxy.local"
|
||||||
BACKEND_SMTP_TLS_MODE: none
|
BACKEND_SMTP_TLS_MODE: none
|
||||||
BACKEND_DOCKER_NETWORK: galaxy-local-dev-net
|
BACKEND_DOCKER_NETWORK: galaxy-local-dev-net
|
||||||
|
BACKEND_STACK_LABEL: local-dev
|
||||||
BACKEND_GAME_STATE_ROOT: /tmp/galaxy-game-state
|
BACKEND_GAME_STATE_ROOT: /tmp/galaxy-game-state
|
||||||
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
|
BACKEND_GEOIP_DB_PATH: /var/lib/galaxy/geoip.mmdb
|
||||||
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.local
|
BACKEND_NOTIFICATION_ADMIN_EMAIL: admin@galaxy.local
|
||||||
@@ -144,6 +155,8 @@ services:
|
|||||||
image: galaxy/gateway:local-dev
|
image: galaxy/gateway:local-dev
|
||||||
container_name: galaxy-local-dev-gateway
|
container_name: galaxy-local-dev-gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
galaxy.stack: local-dev
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -206,6 +219,9 @@ networks:
|
|||||||
galaxy-net:
|
galaxy-net:
|
||||||
name: galaxy-local-dev-net
|
name: galaxy-local-dev-net
|
||||||
|
|
||||||
|
# See note in tools/dev-deploy/docker-compose.yml — labels live only
|
||||||
|
# on services (containers), not on volumes or networks, to keep the
|
||||||
|
# compose config-hash for stateful resources stable across deploys.
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
name: galaxy-local-dev-postgres-data
|
name: galaxy-local-dev-postgres-data
|
||||||
|
|||||||
+8
-83
@@ -106,8 +106,6 @@ addition to the real Mailpit code; see
|
|||||||
for the full runbook (regenerating the dev keypair, switching the
|
for the full runbook (regenerating the dev keypair, switching the
|
||||||
mode off, troubleshooting common boot issues).
|
mode off, troubleshooting common boot issues).
|
||||||
|
|
||||||
The local-dev stack is independent from the local-ci stack below;
|
|
||||||
they bind different ports and can run side by side.
|
|
||||||
|
|
||||||
## Synthetic reports for visual testing (DEV)
|
## Synthetic reports for visual testing (DEV)
|
||||||
|
|
||||||
@@ -159,92 +157,19 @@ record in the parser's `README.md` that the new field cannot be
|
|||||||
derived from legacy text. This keeps the synthetic-mode coverage in
|
derived from legacy text. This keeps the synthetic-mode coverage in
|
||||||
step with the contract as the UI grows.
|
step with the contract as the UI grows.
|
||||||
|
|
||||||
## Local CI verification
|
## CI verification
|
||||||
|
|
||||||
`tools/local-ci/` ships a self-contained Gitea + Actions runner via
|
Workflow changes are exercised on the primary CI host (`gitea.lan`).
|
||||||
docker-compose so workflow changes are exercised end-to-end on a real
|
Push the branch (`git push gitea …`), then open the run in the Gitea
|
||||||
runner before pushing to a remote Gitea instance. On Apple Silicon
|
UI to inspect the status and logs. See `CLAUDE.md` (`## Per-stage CI
|
||||||
the runner and every spawned workflow container are arm64-native
|
gate`) for the per-stage workflow.
|
||||||
(no QEMU). Full runbook lives in
|
|
||||||
[`../../tools/local-ci/README.md`](../../tools/local-ci/README.md);
|
|
||||||
the cheat sheet below covers the operations needed when working a
|
|
||||||
phase that touches CI.
|
|
||||||
|
|
||||||
### Bring up / push / tear down
|
For a sub-second syntax check of a workflow YAML without pulling
|
||||||
|
images or running anything:
|
||||||
```sh
|
|
||||||
make -C tools/local-ci up # idempotent: gitea + runner + admin user + repo
|
|
||||||
make -C tools/local-ci push # add `local-gitea` remote (first call) and push HEAD
|
|
||||||
make -C tools/local-ci status # docker compose ps
|
|
||||||
make -C tools/local-ci logs # tail container logs
|
|
||||||
make -C tools/local-ci down # stop, keep state
|
|
||||||
make -C tools/local-ci clean # stop and wipe volumes for a fresh start
|
|
||||||
```
|
|
||||||
|
|
||||||
Default credentials baked in: `galaxy:galaxy-dev` (admin user, also
|
|
||||||
the owner of the `galaxy/galaxy` repo). Web UI on
|
|
||||||
<http://localhost:3000>; runs at
|
|
||||||
<http://localhost:3000/galaxy/galaxy/actions>.
|
|
||||||
|
|
||||||
### Inspect a run from the shell
|
|
||||||
|
|
||||||
The Gitea Actions API is on `http://localhost:3000/api/v1` with basic
|
|
||||||
auth. Useful for verifying a workflow change without opening the
|
|
||||||
browser:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Latest workflow runs — `status` is a human-readable string here:
|
|
||||||
# "running" / "success" / "failure" / "cancelled".
|
|
||||||
curl -s -u galaxy:galaxy-dev \
|
|
||||||
'http://localhost:3000/api/v1/repos/galaxy/galaxy/actions/tasks?limit=5' \
|
|
||||||
| python3 -m json.tool
|
|
||||||
|
|
||||||
# Tight one-liner for the latest run only:
|
|
||||||
curl -s -u galaxy:galaxy-dev \
|
|
||||||
'http://localhost:3000/api/v1/repos/galaxy/galaxy/actions/tasks?limit=1' \
|
|
||||||
| python3 -c 'import json, sys; r=json.load(sys.stdin)["workflow_runs"][0]; print(r["run_number"], r["status"], r["display_title"])'
|
|
||||||
```
|
|
||||||
|
|
||||||
Step-by-step workflow output is stored zstd-compressed under
|
|
||||||
`/data/gitea/actions_log/galaxy/galaxy/<run_padded>/<job_index>.log.zst`
|
|
||||||
inside the gitea container:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker compose -f tools/local-ci/docker-compose.yml exec -T gitea sh -c '
|
|
||||||
apk add --quiet zstd
|
|
||||||
zstdcat /data/gitea/actions_log/galaxy/galaxy/01/1.log.zst
|
|
||||||
' | less
|
|
||||||
```
|
|
||||||
|
|
||||||
`<run_padded>` is the run number, zero-padded to two digits
|
|
||||||
(`01`, `02`, …); `<job_index>` is the 1-based index of the job
|
|
||||||
inside that run (only `1` for the current single-job workflows).
|
|
||||||
|
|
||||||
### Typical phase workflow
|
|
||||||
|
|
||||||
When a phase changes anything under `.gitea/workflows/` or surfaces
|
|
||||||
new tests in CI:
|
|
||||||
|
|
||||||
1. Local sanity first — run the affected commands directly
|
|
||||||
(`pnpm test`, `pnpm exec playwright test`, the targeted
|
|
||||||
`go test ./...` slice).
|
|
||||||
2. Commit and `make -C tools/local-ci push`.
|
|
||||||
3. Poll the API for the latest run; once it leaves `running`,
|
|
||||||
inspect status. On failure pull the log via the snippet above.
|
|
||||||
4. Fix and repeat. The runner is always-on; each push triggers a
|
|
||||||
fresh run (test cache is cleared by `-count=1` so a green run is
|
|
||||||
honest).
|
|
||||||
|
|
||||||
### Quick syntax-only dry-run with `act`
|
|
||||||
|
|
||||||
For a sub-second check that the workflow YAML is well-formed and
|
|
||||||
action references resolve, without pulling images and without
|
|
||||||
running anything:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
act -W .gitea/workflows/ui-test.yaml -n push
|
act -W .gitea/workflows/ui-test.yaml -n push
|
||||||
```
|
```
|
||||||
|
|
||||||
`act` doesn't honour Gitea-specific behaviours (artifact storage,
|
`act` doesn't honour Gitea-specific behaviours (artifact storage,
|
||||||
secrets, run triggers). Use it for syntax checks; fall back to the
|
secrets, run triggers); use it only for syntax checks.
|
||||||
local Gitea above for honest end-to-end verification.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user