# Agents instructions ## Modifications For any modifications in this repo, ask the user whether to edit on the main branch without commiting or use git worktrees. If using git worktrees: - use the vibe:worktree skill; worktrees live under $VIBE_HOME/worktrees. - create a PR using gh CLI when finished. ## Development environment The root `flake.nix` provides the tool shell and pre-commit checks for the workbench apps. - `nix develop` — tool shell (go 1.27, golangci-lint, gotools, gitleaks, uv). Entering it installs the prek hooks into `.git/hooks/pre-commit`. - Hooks run at commit time: gitleaks (secrets), gofmt (all `workbench/**.go`), and per-app `golangci-lint run` + `go vet ./...` for each Go module under `workbench/`. Hook entries are absolute nix store paths, so they also work from environments without the dev shell on PATH (e.g. VS Code git). - `nix flake check` — runs the full hook set over the repo; run it before pushing. - After changing hooks in `flake.nix`, re-enter `nix develop` once to refresh the installed config. - Staticcheck runs through golangci-lint's `staticcheck` linter (the standalone package is gone from nixpkgs). - The Go hooks need network access to download modules; they work on macOS (relaxed sandbox) and at commit time, but would fail on a strictly-sandboxed Linux CI. ## Repo layout ``` argocd-apps/ ApplicationSet registering every deployed app k8s-apps/ One Helm umbrella chart per app (/Chart.yaml + values.yaml) charts/ Shared Helm charts rendered as extra Application sources workbench/ Go source code + Dockerfiles for self-built images scripts/ Helper scripts (deployed-apps table, pv-diff) docs/superpowers/ Design specs and implementation plans terraform/ Infrastructure provisioning (secrets, external-secrets) ``` ## Secrets Secrets are provisioned by `terraform/secrets/` from the SOPS-encrypted `secrets.yaml`. Keys follow `..`; each `/` becomes one k8s Secret (write-only, values never land in terraform state). Rules: - Agents must never run sops operations themselves (decrypt, encrypt, rotate, edit). Write the terraform/chart wiring only; the user adds sops entries and applies with their key. - To reuse an existing secret in another namespace, the user copies the values into new sops keys under the target namespace (`..`) — the generic provisioner handles the rest. No per-secret terraform resources. - Prefer creating the secret in the target namespace with terraform over external-secrets cross-namespace sharing. ES (ClusterSecretStore per source namespace + RBAC in `k8s-apps/external-secrets/values.yaml` + ExternalSecret in the consumer) is only warranted for wide distribution, e.g. `registry-pull-secret` fanned out to many namespaces. ## Deployed apps An app is deployed if and only if `k8s-apps//appset.yaml` exists (the ApplicationSet git generator matches `k8s-apps/*/appset.yaml`). That file also carries per-app flags: `namespace`, `serverSideApply`, `serverSideDiff`, `scaleToZero`. Undeployed charts live in `k8s-apps/archive/`. New apps pulling from `registry.terence.cloud` need two things, both easy to forget (missing either leaves the app in ImagePullBackOff): their namespace in the `registry-pull-secret` ClusterExternalSecret's `spec.namespaces` in `k8s-apps/registry/values.yaml` — the registry app distributes its own pull secret — and `imagePullSecrets: [{name: registry-pull-secret}]` under `app-template.defaultPodOptions` in the app's own values.yaml. Tool/API documentation for workbench apps lives in each app's `README.md` — update it whenever an app's interface (tool arguments, endpoints, env vars) changes. ## Grafana dashboards Dashboards are code: builders live in `workbench/grafana-dashboards/` (one file per dashboard, registered in `main.go`). Add a builder, run `go run .` from that directory, and commit the regenerated JSON in `k8s-apps/kube-prometheus-stack/dashboards/` together with the code. That app's README has the full workflow. ## Superpowers Never git add things in docs/superpowers forcefully. ## Versioning Never bump image tags in PRs. Tags are bumped by CI on main after merge: 1. A PR changes `workbench//` code; the image tag is not touched. 2. On merge, the `workbench-tag-bump` workflow diffs the merge against the previous tip and, for each changed app without a tag bump, commits `: bump image tag to ` on main: `workbench//build.yaml` plus the image tag in the consuming `k8s-apps//values.yaml`. Sidecar consumers (rangemusique, apachewebdav, gitea-mirror-sync) are updated in the consuming app's values.yaml — the bump script finds all references. 3. The bump commit makes the `build-` ArgoCD application run a Kaniko build in-cluster, pushing `registry.terence.cloud/:` to the in-cluster registry. 4. ArgoCD syncs `k8s-apps/` to the new tag and rolls the Deployment. Caveats: - Never reuse an old tag for new code — `pullPolicy: IfNotPresent` means nodes will keep serving the cached image. Always increment. - Every app carries the `charts/image-wait` chart as a second Application source (wired in `argocd-apps/applicationset.yaml`); it renders PreSync hooks that hold the sync until the workbench image tag exists in the registry, so rollouts never hit ImagePullBackOff while the Kaniko build is still running. - After merging, check the `build-` application in ArgoCD: if the Kaniko build fails, the consuming app's sync fails at the hook and the old pods keep running. - Kaniko rebuilds only happen when the tag changes: the Job name embeds the tag and completed Jobs are kept (ArgoCD prunes the old-tag Job on the next bump), so no same-tag rebuild ever re-runs. - `Chart.yaml` version stays `0.1.0` (never bumped) — the image tag is the release mechanism. ## Golang ### Build artifacts When running `go build`, always delete the generated binary afterwards. ### Linting Always run `golangci-lint run --fix ./...` before considering Go work done. Fix all reported issues. If a lint rule seems wrong, discuss with the user rather than disabling it. ### Static analysis Always run `staticcheck ./...` after linting. Fix all reported issues. ### Code conventions - Flat `package main` layout — no subpackages, no `internal/` directory. - Module path: `github.com/cterence/homelab-gitops/workbench/`. - Go version: 1.27.1 (set in `go.mod`). - `main()` calls a `run() error` function; on error print to stderr and `os.Exit(1)`. - Imports grouped in three blocks separated by blank lines: stdlib, third-party, local (local not needed yet since everything is `package main`). - Error handling: wrap with context using `fmt.Errorf("doing X: %w", err)`. Never both log and return an error — pick one. - Logging: use `log/slog` with a JSON handler (`slog.NewJSONHandler(os.Stdout, nil)`). Structured key-value pairs only. - `context.Context` as first parameter on all methods that do I/O or are long-running. - Concurrency: prefer `errgroup.WithContext` with `g.SetLimit()` for bounded parallelism. - Graceful shutdown: use `signal.NotifyContext` for signal handling. ### Tests - Test files live alongside source as `_test.go` in the same `package main`. - Table-driven tests using anonymous structs with `t.Run(tt.name, ...)`. - Tests cover pure functions (parsing, comparison, logic) — no integration tests. - Run tests with `go test ./...` from the app directory. - Use `t.Setenv` for environment-dependent tests. ### Dockerfiles - Multi-stage build: `golang:1.27-alpine` (or pinned by digest) as build stage, `scratch` or `gcr.io/distroless/static-debian12:nonroot` as final. - Build flags: `CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w"`. - No Makefiles. Build is entirely Dockerfile-based; Kaniko builds in-cluster. ## Helm charts (k8s-apps/) - Each app directory is an umbrella chart: `Chart.yaml` + `values.yaml`. - `Chart.yaml`: `apiVersion: v2`, `version: 0.1.0` (never bumped), dependencies on upstream charts. - Most common dependency: `app-template` (bjw-s-labs). - Workbench app images reference the in-cluster registry: `registry.terence.cloud/` with a `tag` matching `build.yaml`. - Third-party images are pinned by digest (`tag@sha256:...`). ## Falco (k8s-apps/falco/) - In Falco, a macro redefined in a later rules file overrides the earlier definition (unlike lists, which append). Never define the same macro name in more than one `customRules` entry — merge all branches into a single `condition` with `or`. A duplicate silently kills the earlier whitelist (this caused the kaniko "Read sensitive file untrusted" alert spam; see PR #1272). - Validate changes with `scripts/validate-falco-rules.sh` (needs docker + yq). ## Comments Do not write comments unless there is a sensible justification for one. ## Commit messages Format: `: ` — lowercase, short, imperative. Example: `cnpg-restore-test: fix rbac` ## Renovate Config is at `/renovate.json5`. Auto-merge is enabled for non-major updates and digests. Renovate uses the `fix:` conventional-commit prefix. ## CI - `update-deployed-apps.yaml` — regenerates the deployed apps table in README.md on push to main. - `nix-cache.yaml` — builds every flake package per system and pushes to the niks3 binary cache on push to main touching the flake or `workbench/catbox/**`. On PRs, the `catbox-pr` job runs on every PR and builds `.#catbox`, `.#catbox-android-bin` and `.#catbox-android` (pushing them to the cache) when the PR touches catbox's Go module, the nix/android trees, the flake, or the workflow — this is the only place the go toolchain, vendorHash, and gradle set (AGP, compose-bom, verification metadata, FOD hash) are proven before merge. - `pr-argo-diff.yaml` — runs `argocd app diff` per changed app on PRs touching `k8s-apps/**` and posts the diff as a PR comment. - `workbench-ci.yaml` — on PRs touching `workbench/**`: runs `go vet`/`go test`/golangci-lint per changed Go app, pytest per changed Python app, and a no-push docker build mirroring the Kaniko context. A changed Go app with an `integration_test.go` also runs it gated with `_INTEGRATION=1` (helper-process e2e; needs the DERP network, available on hosted runners). - `workbench-tag-bump.yaml` — on push to main touching `workbench/**`: bumps `build.yaml` (and the matching `k8s-apps` image tag) when a merge modified a workbench app without bumping its tag. Idempotent (any `+tag:` line in the merge window skips the bump), runs serialized, works with Renovate merges too. - Image builds still happen in-cluster via Kaniko on merge to main; CI builds are validation only. - Branch protection on main requires the `Workbench CI result` and `argocd_diff` checks before merging (both workflows run on every PR; the workbench one reports a stable aggregate because matrix job names vary per app). With `platformAutomerge`, Renovate's auto-merge only merges once these pass.