chore(ci): tidy CI/dev infra — drop local-ci, lift migration rule, scope by galaxy.stack label
Five connected cleanups across the dev/CI infrastructure:
1. Drop tools/local-ci/. The standalone Gitea + act_runner stack was
the legacy "offline workflow validator"; the per-stage CI gate now
runs on gitea.lan and the directory was only retained as a
fallback. Removing it leaves no operational dependency: backend,
gateway, and game code have no references; documentation that
pointed at it (CLAUDE.md, docs/ARCHITECTURE.md, ui/docs/testing.md,
tools/dev-deploy/README.md, tools/local-dev/README.md) is updated
in this same change. Historical "Verified on local-ci run N"
markers in ui/PLAN.md are preserved unchanged.
2. Lift the pre-production single-migration rule. The rule forced
every schema delta into 00001_init.sql and required a manual
make clean-data wipe on every backward-incompatible change in
tools/dev-deploy/. Future schema deltas now land as additive
sequence-numbered files (00002_*.sql, …) that goose applies
automatically on backend startup; 00001_init.sql becomes an
immutable baseline. Authoring conventions live in
backend/internal/postgres/migrations/README.md. The chain may be
squashed back into a fresh 00001 as a deliberate one-time
operation before the first production deployment.
3. Document the deployment cadence. The dev environment is
single-tenant: pushes to feature/* run the test workflows
(go-unit, ui-test, integration) only; dev-deploy.yaml fires on
push to development. A workflow_dispatch override on
dev-deploy.yaml lets a developer preview a feature branch on the
shared dev environment before merge; the next merge into
development overwrites the manual deploy idempotently.
4. Scope compose-managed resources by an explicit
galaxy.stack=<local-dev|dev-deploy> label. Both compose files
stamp the label on every service, network, and named volume.
Makefiles in tools/local-dev/ and tools/dev-deploy/ filter their
engine-cleanup operations by (stack-label AND engine OCI title)
so they never touch unrelated workloads on the same daemon.
dev-deploy.yaml gains a pre-`compose up` step that reaps stale
exited/dead containers under the dev-deploy stack label.
5. Backend now stamps the same galaxy.stack=<value> label on every
engine container it spawns, sourced from a new BACKEND_STACK_LABEL
env var (empty → label not applied; legacy-safe). Both compose
files set it to their stack name (local-dev / dev-deploy). The
contract is recorded in docs/ARCHITECTURE.md under
"Container labels". A package-level test in
backend/internal/runtime exercises both the label-present and
label-absent paths.
No tests intentionally regressed: go test ./backend/internal/{config,
runtime,dockerclient} is green, both compose files validate cleanly,
and the backend, gateway, and game modules all build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -537,10 +537,7 @@ func (s *Service) runStart(ctx context.Context, op OperationLog) error {
|
||||
Env: map[string]string{
|
||||
"GAME_STATE_PATH": statePath,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"galaxy.game_id": gameID.String(),
|
||||
"galaxy.engine_version": version.Version,
|
||||
},
|
||||
Labels: s.engineLabels(gameID.String(), version.Version),
|
||||
BindMounts: []dockerclient.BindMount{
|
||||
{
|
||||
HostPath: hostStatePath,
|
||||
@@ -735,10 +732,7 @@ func (s *Service) runPatch(ctx context.Context, op OperationLog, target EngineVe
|
||||
Env: map[string]string{
|
||||
"GAME_STATE_PATH": statePath,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"galaxy.game_id": op.GameID.String(),
|
||||
"galaxy.engine_version": target.Version,
|
||||
},
|
||||
Labels: s.engineLabels(op.GameID.String(), target.Version),
|
||||
BindMounts: []dockerclient.BindMount{
|
||||
{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.
|
||||
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
|
||||
// responds 2xx or until the timeout elapses. The Docker daemon
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user