make dev-up # creates kind cluster, builds images, installs Helm chart
make web-dev # starts the Vite dev server with proxy to in-cluster APIAny change to the web dashboard's visual surface starts in
design.pen (Pencil), not in code. Open design.pen, update the
relevant screen, review with the maintainers, then translate to
React. This keeps the 18 designed screens the source of truth and
prevents the dashboard from drifting into code-led redesigns.
Backend/operator/API changes do not need a Pencil pass.
The dashboard is built on HeroUI (@heroui/react, v3) rather than hand-rolled or Radix primitives. When a screen needs a stock control (button, table, modal, form field, popover, etc.), import it directly from @heroui/react — see the HeroUI docs for the current v3 API surface. When a pattern recurs across screens (a confirm dialog, a status chip, a stat card, the authenticated shell), reuse or extend the Gameplane composition in web/src/components/ui/ rather than re-wrapping HeroUI from scratch. New reusable compositions belong there too, each with a co-located *.test.tsx. Don't add @radix-ui/* or class-variance-authority — the dashboard's HeroUI rebuild (specs/done_014-heroui-web-rebuild/) removed both from web/package.json.
- Indentation: tabs in Go, 2 spaces elsewhere (YAML, JSON, TypeScript, Markdown). LF line endings everywhere.
- Go:
gofmt,go vet,golangci-lint. Errors are wrapped with%wto preserve the cause chain. - Linting: Fix the underlying issue; do not add inline suppression directives (
//nolint:,// eslint-disable). A few centralized exemptions exist in.golangci.yml(e.g.,_test.gofiles skip errcheck/gosec/unparam, and controller builders skip revive's exported rule) — contributors should not add new inline exemptions on top of these. - TypeScript: strict mode on. ESLint + Prettier. No
anywithout a justification comment. - Comments: default to writing none — let naming carry the weight. Add one only when the why is non-obvious (hidden invariant, workaround, or constraint a reader would ask about).
This repo pins every external GitHub Action to a full 40-hexadecimal commit SHA with a # vX.Y.Z trailing comment, enforced by the zizmor security scanner in the workflow-lint gate. If you're modifying a workflow or adding a new action:
- Resolve the action's commit SHA using:
git ls-remote --tags --refs https://github.com/<owner>/<repo>
- Pin it in the workflow file:
- uses: owner/action@<40-hex-SHA> # vX.Y.Z
- Dependabot's
github-actionsentry keeps all pins current — do not bump them by hand.
Before pushing changes to .github/workflows/ or .github/actions/, run the workflow-lint checks locally (the one exception to the "don't run suites locally" rule, since they are static checks):
go install github.com/rhysd/actionlint/cmd/actionlint@latest
actionlint .github/workflows/*.yaml
uv tool install zizmor
zizmor .github/workflows/ .github/actions/actionlint catches workflow schema errors and runs shellcheck against run: bodies. zizmor verifies SHA pinning, least-privilege permissions, dangerous triggers, and other security concerns.
Every job in every workflow must have:
- An explicit
permissionsblock granting only what that job needs (top-level default iscontents: read; anything more is granted only to the specific job that needs it) - An explicit
timeout-minutes(default budget is ≤30 minutes; anything above it requires an inline justification comment)
Documented exceptions with longer timeouts are the five e2e jobs (60 minutes each) and the image build in publish-edge.yaml (35 minutes).
Run the whole suite:
make testPer-component:
cd netguard && go test ./...
cd gameaction && go test ./...
cd gameproto && go test ./...
cd svcutil && go test ./...
cd operator && go test ./...
cd api && go test ./...
cd agent && go test ./...
cd audit-syslog-bridge && go test ./...
cd telemetry-receiver && go test ./...
cd sentinel && go test ./...
cd capture-sidecar && go test ./...
cd tunnel && go test ./...
cd gp-module && go test ./...
cd mcp-server && go test ./...
cd web && npm testThe e2e suite runs against a real kind cluster. CI splits it into parallel
buckets defined in test/e2e/buckets.sh — the e2e bucket coverage job fails on
any test that isn't in one, so add new tests to a bucket.
The two game-bot tests (TestGameServer_MinecraftBotConnects,
TestGameServer_TerrariaBotConnects) boot a real game server and run a
headless protocol bot as a Job inside the cluster, dialing the game
Service's DNS name — the same network path a real in-cluster client uses —
rather than tunnelling through kubectl port-forward.
The games namespace carries a default-deny-egress NetworkPolicy whose
podSelector: {} matches every pod in it. This allows DNS resolution but
blocks outbound connections, so an in-cluster helper pod placed there cannot
connect to the game Service. The probe Job therefore runs in the default
namespace, where no NetworkPolicy restricts it. Note that kubectl port-forward bypasses NetworkPolicy entirely (it tunnels kubelet→pod), so
"it works via port-forward" is not evidence that in-cluster pod→game traffic
works.
The bot is test/e2e/cmd/gameprobe, built into gameplane-test/gameprobe:e2e
by the e2e-gameprobe bake target. That target sits outside the e2e bake
group so only the game-bot job pays to build it, and deploy/kind/e2e.sh
side-loads it into the cluster when present. The e2e game bot (kind) CI job
is now blocking rather than advisory.
The ingress-nginx smoke (kind) CI job is separate from the buckets above: it
boots a Kind cluster on the same node config make dev-up uses and applies
the dev ingress-nginx manifest pinned in deploy/kind/up.sh
(INGRESS_NGINX_VERSION), waiting for the controller Deployment's rollout
to finish (its replica available).
It only runs when deploy/kind/** or the CI workflow file itself changes.
Much of this codebase is developed with AI coding assistants (Claude
Code). The project was started on Claude Opus 4.8 (claude-opus-4-8);
since June 2026 development continues on Claude Fable 5
(claude-fable-5). Agent-facing guidance lives in
CLAUDE.md — keep it current when project structure or
house rules change. All contributions, AI-assisted or not, go through
the same review, lint, and test gates below.
- Fork + feature branch
- Do not run
make testormake lintlocally. A quick compile check (go build ./...ortsc --noEmit) is fine to avoid pushing obviously-broken code, but the full test and lint suites run only on CI (GitHub Actions) — that is where they are gated and where failures surface. The exception is workflow changes: if you modify.github/workflows/or.github/actions/, runactionlintandzizmorlocally before pushing (see CI and workflows section above). - For UI work, include the Pencil node id(s) touched in the PR description
- Sign commits (
git commit -s) - PR labels: every PR must carry at least one
type:and onearea:label. CodeRabbit (see Code review below) applies them automatically, and theauto-labelworkflow (hack/auto_label.py, every 15 minutes) fills in a missingtype:from the conventional-commit title prefix and a missingarea:from the changed paths;type: securityis never derived. You are responsible for verifying they are correct. Thetype:taxonomy isfeature/fix/refactor/test/ci/chore/docs/security. Thearea:taxonomy isoperator/api/agent/web/modules/chart/e2e/specs/shared/optional-components. A breaking CRD, API, or chart-value change also takesbreaking. Thestatus:labels (blocked,needs-maintainer,in-progress) are optional. A PR spanning several components takes severalarea:labels rather than being left unlabelled.
Game-module changes (modules/) belong in the separate GameplanePanel/module
repo, which this repo vendors as a submodule. Open the module PR there; once it
merges, bump the submodule pointer here (git add modules) in a follow-up PR.
.gitmodules points both submodules (modules/, website/) at the upstream
GitHub URLs, so a fork of this repo clones them without forking them too. To
work on your own fork of a submodule, repoint it locally:
git submodule set-url modules https://github.com/<you>/module.git
(don't commit that change).
PRs receive an automated advisory review from the CodeRabbit GitHub App, configured by .coderabbit.yaml. CodeRabbit's rules encode the repo's style guide and house rules — wrapping errors with %w, no unjustified any, fix rather than silence linter warnings, etc. Its feedback does not block merges. If you think a rule is wrong, raise it with the maintainers rather than working around it — this keeps the rules canonical and consistent across the codebase.
Tags matching v* trigger the release.yaml workflow, which builds the
component images, pushes the Helm chart to the gameplane OCI registry, and
keyed-cosign-signs all images (component and chart) and official modules/*
bundles by digest, recording all signatures in the public Sigstore Rekor
transparency log.
Signing is mandatory and fail-closed: if COSIGN_PRIVATE_KEY is not
configured, the release job fails before anything is pushed. A one-time key
setup is required: run cosign generate-key-pair, set
COSIGN_PRIVATE_KEY/COSIGN_PASSWORD as secrets of the release-signing
environment, and publish signing/cosign.pub. See
module-authoring.md for details.
Every job that signs (release.yaml, publish-edge.yaml, images.yaml and
republish-modules.yaml) runs in the release-signing environment. Its
deployment policy admits only master and v* tags, so a run on any other ref
never receives the key. Images are pushed by digest, signed and verified, and
only then tagged, so a published tag always names a signed image. The image
matrix does not cancel sibling legs when one fails. A leg that fails before tag
attachment leaves at most an untagged digest behind. A leg whose oras tag
succeeded but whose later oras resolve check failed may leave published tags,
so inspect the image's tags before re-running the failed jobs.
External Directory Submissions
The project maintains a tracked list of external directory submissions (AlternativeTo, Awesome-Selfhosted, Awesome-Kubernetes) to grow visibility and discoverability. See the outreach tracker for submission status and history.