From 61cf21edeee55f97ada1f6788b2151a01835d0d7 Mon Sep 17 00:00:00 2001 From: Hien Vu Date: Fri, 31 Jul 2026 13:38:06 +0900 Subject: [PATCH 1/6] chore(claude): add dev container skills for Claude Code Add three project skills that route development actions through the Bucketeer dev container, where the toolchain is guaranteed: - devc: detect the running dev container (local devcontainer or GitHub Codespace) and run any command inside it via scripts/devc-exec.sh - devc-generate: proto/mock generation inside the container (protoc v23.4 guaranteed; host version mismatch churns every generated file) - devc-deploy: minikube deploys inside the container, incl. the faster single-service rollout path Also update .claude/CLAUDE.md to prefer the dev container + minikube flow for these actions, with Docker Compose as the host-based alternative. Co-Authored-By: Claude Fable 5 --- .claude/CLAUDE.md | 24 +++-- .claude/skills/devc-deploy/SKILL.md | 87 ++++++++++++++++ .claude/skills/devc-generate/SKILL.md | 62 ++++++++++++ .claude/skills/devc/SKILL.md | 75 ++++++++++++++ .claude/skills/devc/scripts/devc-exec.sh | 120 +++++++++++++++++++++++ 5 files changed, 361 insertions(+), 7 deletions(-) create mode 100644 .claude/skills/devc-deploy/SKILL.md create mode 100644 .claude/skills/devc-generate/SKILL.md create mode 100644 .claude/skills/devc/SKILL.md create mode 100755 .claude/skills/devc/scripts/devc-exec.sh diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 2575a1f99e..0f30d359fd 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -89,10 +89,26 @@ make mockgen # Regenerate only mocks (after changing interfaces that h - Run `make proto-all` after any `.proto` file change — this regenerates Go bindings and OpenAPI/Swagger specs. - Run `make mockgen` after changing any Go interface that has generated mocks in a `mock/` directory. - The generated files are committed to the repo. `protoc` v23.4 must be on PATH ahead of any other version for the version header in `.pb.go` files to stay at `v4.23.4`. +- Prefer running generation inside the dev container via the `devc-generate` skill — the container guarantees protoc v23.4, while a host version mismatch churns every generated file. ### Local development -**Docker Compose (recommended for most development):** +**Dev container + Minikube (preferred):** + +Development actions — proto generation, builds, deploys, kubectl/helm — should run **inside the Bucketeer dev container**, where tool versions are guaranteed (protoc v23.4, go-tools, minikube/helm/kubectl). Check for a running dev container first before running these on the host. Use the project skills: + +- `devc` — detect the running dev container (local devcontainer or Codespace) and run any command inside it: `bash .claude/skills/devc/scripts/devc-exec.sh status` +- `devc-generate` — proto/mock generation inside the container +- `devc-deploy` — deploy to the minikube cluster inside the container + +```bash +make start-minikube # Inside the dev container. Always use this, not `minikube start` directly +make deploy-bucketeer # Deploy all Helm charts to minikube +``` + +Never run kubectl/helm bare on the host — the host kubectl context may point at a real GKE cluster, not minikube. + +**Docker Compose (host-based alternative):** ```bash make docker-compose-up # Start all services make docker-compose-status # Check status @@ -107,12 +123,6 @@ Add to `/etc/hosts`: 127.0.0.1 api-gateway.bucketeer.io ``` -**Minikube (Kubernetes-based):** -```bash -make start-minikube # Always use this, not `minikube start` directly -make deploy-bucketeer # Deploy all Helm charts -``` - ### Database migrations ```bash make migration-validate # Validate migration files with Atlas diff --git a/.claude/skills/devc-deploy/SKILL.md b/.claude/skills/devc-deploy/SKILL.md new file mode 100644 index 0000000000..21963d5ed8 --- /dev/null +++ b/.claude/skills/devc-deploy/SKILL.md @@ -0,0 +1,87 @@ +--- +name: devc-deploy +description: >- + Build and deploy Bucketeer to the minikube cluster inside the dev container, + or redeploy/restart a single service there. Use this whenever the user wants + to deploy locally, run "make deploy-bucketeer", start minikube, get their + code changes running in the dev cluster, restart a crashing pod, or says + "devc-deploy", "deploy to minikube", "redeploy the backend". Also use it to + check deployment health (pods not ready, gateway not responding) in the dev + container environment. +--- + +# devc-deploy — deploy Bucketeer inside the dev container + +Deployment target is the minikube cluster *inside* the dev container (helm +charts in `manifests/`), not the host docker-compose stack. All commands go +through the devc wrapper (see the `devc` skill): + +```bash +DEVC="bash .claude/skills/devc/scripts/devc-exec.sh" +``` + +## 1. Preflight + +```bash +$DEVC status +``` + +- `dockerd` not running → start it (command is in the status output) — image + builds need it. +- `minikube` not running → `$DEVC 'make start-minikube'`. Never `minikube start` + directly. If minikube IS already running, skip this: the target intentionally + exits 1 with "minikube is already running" — that is not an error to fix. + +## 2. Full deploy + +```bash +$DEVC 'make deploy-bucketeer' +``` + +What it does, so failures are diagnosable: uninstalls the existing `bucketeer` +helm release → regenerates cert/token/oauth secrets → builds all Go binaries → +builds docker images with `TAG=localenv` → loads them into minikube → helm +install/upgrade `localenv` (MySQL, Redis, Pub/Sub emulator, optionally +Postgres/BigQuery emulator) → helm install `bucketeer` with +`manifests/bucketeer/values.dev.yaml`. + +This takes many minutes. Run it with a 600000 timeout or `run_in_background` +and monitor. Postgres/BigQuery enablement is auto-detected from +`dataWarehouse` in `values.dev.yaml` — don't set it manually, but remember the +invariant: `web` and `subscriber` must use the same event store, so data +warehouse changes belong in `values.dev.yaml`, not ad-hoc helm flags. + +## 3. Single service — faster than a full deploy + +For a code change to one service (e.g. backend): + +```bash +$DEVC 'make build-go-embed && TAG=localenv make build-docker-images && TAG=localenv make minikube-load-images' +$DEVC 'kubectl rollout restart deployment && kubectl rollout status deployment ' +``` + +The Bucketeer deployments are `api`, `web`, `batch-server`, and `subscriber` +(confirm with `$DEVC 'kubectl get deployments'`). Chart-level +changes instead: `helm upgrade bucketeer manifests/bucketeer/ --values +manifests/bucketeer/values.dev.yaml`. + +## 4. Verify + +```bash +$DEVC 'kubectl get pods' # everything Running/Completed, restarts not climbing +$DEVC 'curl -sk https://web-gateway.bucketeer.io/health' # must run INSIDE the container +``` + +The `*.bucketeer.io` hosts entries live in the container's `/etc/hosts` +(pointed at `minikube ip`) — curl from the host proves nothing. For a failing +pod: `$DEVC 'kubectl logs deploy/ --tail=100'` and +`$DEVC 'kubectl describe pod '`. + +## Related dev-cluster chores + +- Bootstrap e2e accounts (after a fresh deploy, before e2e tests): + `$DEVC 'make create-dev-container-e2e-accounts'` +- Wipe e2e data: `$DEVC 'make delete-dev-container-mysql-data'` (or the + `-postgres-` variant). These are destructive — confirm with the user first. +- MySQL from inside the container: host `$(minikube ip)`, port 32000, + user/pass `bucketeer`, db `bucketeer`. diff --git a/.claude/skills/devc-generate/SKILL.md b/.claude/skills/devc-generate/SKILL.md new file mode 100644 index 0000000000..bf2a688881 --- /dev/null +++ b/.claude/skills/devc-generate/SKILL.md @@ -0,0 +1,62 @@ +--- +name: devc-generate +description: >- + Regenerate Bucketeer protobuf Go bindings, OpenAPI/Swagger specs, and gomock + files inside the dev container, where protoc is guaranteed to be exactly + v23.4. Use this whenever a .proto file changed, generated *.pb.go / + *.pb.gw.go / swagger files need regenerating, a mocked Go interface changed + (mockgen), or the user says "generate proto", "regen protos", "make + proto-all", "make mockgen", or "devc-generate". Prefer this over running + protoc or make proto-all on the host — a host protoc version mismatch + silently rewrites every generated file's header. +--- + +# devc-generate — code generation inside the dev container + +Generated files are committed to the repo, and their headers record the protoc +version (`protoc v4.23.4`). The dev container ships exactly protoc 23.4, so +generation must happen there; a different host protoc churns every `.pb.go` +file and the PR becomes unreviewable. + +All commands below go through the devc wrapper (see the `devc` skill for how +detection works): + +```bash +DEVC="bash .claude/skills/devc/scripts/devc-exec.sh" +``` + +## 1. Pick the right target + +| What changed | Target | +|---|---| +| `.proto` files | `make proto-all` | +| A Go interface that has generated mocks in a `mock/` dir | `make mockgen` | +| Both, or unsure | `make generate-all` | + +## 2. Run it + +```bash +$DEVC 'make proto-all' # or make mockgen / make generate-all +``` + +This is minutes-long; use a generous Bash timeout (600000). If the run fails +on protolock, the `.proto` change broke backward compatibility — read the +error; don't force it without flagging the compatibility break to the user. + +## 3. Verify before declaring success + +- `git status --porcelain` (on the host for a local devcontainer; inside via + `$DEVC 'git status --porcelain'` for a codespace) — the changed files should + be only the ones related to your proto/interface change plus their generated + outputs. **A diff touching every `.pb.go` in the repo means a wrong protoc + version — abort and check `$DEVC 'protoc --version'`.** +- Spot-check one regenerated file's header still says `protoc v4.23.4`: + `grep -m1 "protoc " proto//.pb.go` +- Build still compiles: `$DEVC 'make build-go'` (or the affected + `make build-`), and `$DEVC 'make gofmt'` after any Go changes. + +## Codespace caveat + +In a codespace the regenerated files land in the codespace's clone, not the +host repo. Commit/push from inside, or copy back with `gh codespace cp`. The +`status` subcommand of the devc script tells you which mode you're in. diff --git a/.claude/skills/devc/SKILL.md b/.claude/skills/devc/SKILL.md new file mode 100644 index 0000000000..61e4ae79cf --- /dev/null +++ b/.claude/skills/devc/SKILL.md @@ -0,0 +1,75 @@ +--- +name: devc +description: >- + Detect the running Bucketeer dev container (local VS Code devcontainer or + GitHub Codespace) and run commands inside it. Use this whenever a task should + run in the dev container environment — make targets, builds, tests, kubectl / + helm / minikube commands, checking whether the container is up — or when the + user says "devc", "dev container", "devcontainer", "codespace", or "run this + inside the container". Also use it when a task needs tools the container + guarantees but the host may lack (protoc 23.4, mockgen, protolock, helm, + kubectl, minikube). devc-generate and devc-deploy build on this skill. +--- + +# devc — run commands inside the Bucketeer dev container + +The dev container is the canonical Bucketeer development environment: Ubuntu with +docker-in-docker, minikube + helm + kubectl, protoc v23.4, and Go tooling in +`/home/codespace/go-tools/bin` (a persistent volume, NOT on PATH in plain +non-login shells). The workspace is `/workspaces/bucketeer`, the user is +`codespace` (passwordless sudo). + +## How to run anything inside it + +Always go through the wrapper script — it finds the container and sets up PATH: + +```bash +# Where is the container, and is the environment healthy? +bash .claude/skills/devc/scripts/devc-exec.sh status + +# Run any command in /workspaces/bucketeer inside the container +bash .claude/skills/devc/scripts/devc-exec.sh 'make build-api' +bash .claude/skills/devc/scripts/devc-exec.sh 'kubectl get pods' +``` + +Detection order (the script handles all of this): +1. Already inside the container (`/workspaces/bucketeer` exists, user `codespace`) → run directly. +2. Local devcontainer → `docker ps` filtered by label `devcontainer.local_folder=`, exec via `docker exec`. +3. GitHub Codespace → `gh codespace list` (needs the `codespace` auth scope), exec via `gh codespace ssh`. + +Exit code 2 means no container was found; the script prints how to start one. +Don't fall back to running the command on the host in that case — tell the user +and let them choose, because host tool versions (especially protoc) may differ. + +## Local devcontainer vs Codespace — the one difference that matters + +- **Local devcontainer**: `/workspaces/bucketeer` is a bind mount of the host + repo. Files generated inside appear in the host working tree immediately. +- **Codespace**: a separate clone. Generated or edited files stay in the + codespace. To get them back: commit and push from inside, or + `gh codespace cp 'remote:/workspaces/bucketeer/' `. + Always tell the user which mode you're in when file changes are involved + (`status` prints it). + +## Environment facts and gotchas + +- Long commands (image builds, deploys) can take many minutes — use a generous + Bash timeout (600000) or `run_in_background`. +- `dockerd` inside the container is started by the post-attach hook, but that + only fires when an editor attaches. If `status` says it's not running: + `bash .claude/skills/devc/scripts/devc-exec.sh 'nohup sudo dockerd > /tmp/dockerd.log 2>&1 & sleep 5 && docker info > /dev/null && echo ok'` +- minikube must be started with `make start-minikube`, never `minikube start` + directly (the make target restores the cluster config and localenv services). + Note: `make start-minikube` intentionally **exits 1 if minikube is already + running** — check `minikube status` first instead of treating that as failure. +- `web-gateway.bucketeer.io` / `api-gateway.bucketeer.io` resolve via the + container's own `/etc/hosts` (pointed at `minikube ip`). Health checks with + curl against those hosts must run *inside* the container, not on the host. +- If go-tools are missing or permissions look broken, the fix is the setup + script: `bash .devcontainer/setup.sh` (idempotent, cache-aware). +- **Never run kubectl/helm bare on the host for dev work.** The host's kubectl + context may point at a real GKE cluster, not minikube — always go through the + wrapper so commands hit the cluster inside the container. +- The host may also run a docker-compose Bucketeer stack in parallel + (`docker-compose/compose.yml`). That is a different environment — this skill + is only about the dev container / minikube world. diff --git a/.claude/skills/devc/scripts/devc-exec.sh b/.claude/skills/devc/scripts/devc-exec.sh new file mode 100755 index 0000000000..e9873e7420 --- /dev/null +++ b/.claude/skills/devc/scripts/devc-exec.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Locate the Bucketeer dev container and run a command inside it. +# +# Usage: +# devc-exec.sh status Report where the dev container is and its health +# devc-exec.sh Run a command in /workspaces/bucketeer inside it +# +# Exit codes: 0 success, 2 no dev container found, otherwise the command's exit code. +set -euo pipefail + +WORKDIR=/workspaces/bucketeer +# go-tools live in a persistent volume; not on PATH in non-login shells +SETUP_PATH='export PATH=/home/codespace/go-tools/bin:$PATH' + +MODE="" +CID="" +CODESPACE="" + +detect() { + # Case 1: this shell is already inside the dev container + if [ -d "$WORKDIR" ] && [ "$(id -un)" = "codespace" ]; then + MODE=inside + return + fi + + # Case 2: local devcontainer (VS Code "Reopen in Container" / devcontainer CLI) + local repo_root + repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + CID="$(docker ps -q --filter "label=devcontainer.local_folder=$repo_root" 2>/dev/null | head -1 || true)" + if [ -z "$CID" ]; then + # Fallback: any running devcontainer whose local folder looks like a bucketeer checkout + CID="$(docker ps --filter "label=devcontainer.local_folder" \ + --format '{{.ID}} {{.Label "devcontainer.local_folder"}}' 2>/dev/null \ + | awk '$2 ~ /bucketeer/ {print $1; exit}' || true)" + fi + if [ -n "$CID" ]; then + MODE=local + return + fi + + # Case 3: GitHub Codespace (requires `gh` with the codespace scope) + CODESPACE="$(gh codespace list --json name,repository,state \ + -q '.[] | select((.repository | endswith("/bucketeer")) and .state == "Available") | .name' 2>/dev/null \ + | head -1 || true)" + if [ -n "$CODESPACE" ]; then + MODE=codespace + return + fi +} + +run_inside() { + local cmd="$1" + case "$MODE" in + inside) + bash -c "cd $WORKDIR && $SETUP_PATH && $cmd" + ;; + local) + docker exec -u codespace -w "$WORKDIR" "$CID" bash -c "$SETUP_PATH && $cmd" + ;; + codespace) + gh codespace ssh -c "$CODESPACE" -- "cd $WORKDIR && $SETUP_PATH && $cmd" + ;; + esac +} + +not_found() { + echo "No running Bucketeer dev container found." >&2 + echo "" >&2 + echo "Checked: this shell, local devcontainers (docker label devcontainer.local_folder)," >&2 + echo "and GitHub Codespaces (gh codespace list)." >&2 + echo "" >&2 + echo "To start one:" >&2 + echo " - VS Code: 'Dev Containers: Reopen in Container' on this repo" >&2 + echo " - CLI: devcontainer up --workspace-folder ." >&2 + echo " - Codespace: gh codespace create -R bucketeer-io/bucketeer" >&2 + if ! gh codespace list >/dev/null 2>&1; then + echo "" >&2 + echo "Note: 'gh codespace list' failed — if you use Codespaces, grant the scope with:" >&2 + echo " gh auth refresh -h github.com -s codespace" >&2 + fi + exit 2 +} + +status_report() { + case "$MODE" in + inside) echo "mode: inside (this shell is already in the dev container)" ;; + local) echo "mode: local devcontainer (docker exec, container $CID)" + echo "workspace: bind-mounted from the host — file changes appear in the host repo directly" ;; + codespace) echo "mode: GitHub Codespace '$CODESPACE' (gh codespace ssh)" + echo "workspace: SEPARATE clone — changes made inside do NOT appear in the host repo" ;; + esac + echo "---" + run_inside ' + echo "user: $(id -un) workdir: $(pwd)" + echo "protoc: $(protoc --version 2>/dev/null || echo MISSING)" + command -v mockgen >/dev/null && echo "go-tools: OK" || echo "go-tools: MISSING (run bash .devcontainer/setup.sh)" + docker info >/dev/null 2>&1 && echo "dockerd: running" || echo "dockerd: NOT running (start with: nohup sudo dockerd > /tmp/dockerd.log 2>&1 &)" + if minikube status >/dev/null 2>&1; then + # Ignore transient states (Pending/ContainerCreating/Init) — batch CronJobs + # constantly spawn short-lived pods and would make the count flap. + pods=$(kubectl get pods --no-headers 2>/dev/null) + total=$(echo "$pods" | grep -c . || true) + failing=$(echo "$pods" | grep -cE "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" || true) + echo "minikube: running ($total pods, $failing failing)" + [ "$failing" -gt 0 ] && echo "$pods" | grep -E "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" + else + echo "minikube: NOT running (start with: make start-minikube — never minikube start)" + fi + echo "git: $(git status --porcelain | wc -l | tr -d " ") modified files on branch $(git branch --show-current)" + ' +} + +detect +[ -z "$MODE" ] && not_found + +if [ "$#" -eq 0 ] || [ "$1" = "status" ]; then + status_report +else + run_inside "$*" +fi From f53ffa2865df84e1caeb6d84ac487d27f6f7840a Mon Sep 17 00:00:00 2001 From: Hien Vu Date: Fri, 31 Jul 2026 13:44:06 +0900 Subject: [PATCH 2/6] fix: harden devc-exec.sh detection and status reporting Address review comments on PR #2741: - Guard kubectl get pods in the status report; print a clear message when kubectl fails while minikube is up instead of a misleading 0-pod count - Derive the repo root from the script location for an exact devcontainer.local_folder label match from any cwd; drop the fuzzy fallback that could pick the wrong container when multiple checkouts are running Co-Authored-By: Claude Fable 5 --- .claude/skills/devc/scripts/devc-exec.sh | 26 ++++++++++++------------ manifests/datadog-agent.yaml | 20 ++++++++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 manifests/datadog-agent.yaml diff --git a/.claude/skills/devc/scripts/devc-exec.sh b/.claude/skills/devc/scripts/devc-exec.sh index e9873e7420..af0ee10cbc 100755 --- a/.claude/skills/devc/scripts/devc-exec.sh +++ b/.claude/skills/devc/scripts/devc-exec.sh @@ -23,16 +23,13 @@ detect() { return fi - # Case 2: local devcontainer (VS Code "Reopen in Container" / devcontainer CLI) + # Case 2: local devcontainer (VS Code "Reopen in Container" / devcontainer CLI). + # Derive the repo root from this script's location (/.claude/skills/devc/scripts/) + # so the exact label match works from any cwd — no fuzzy fallback that could pick + # the wrong container when multiple checkouts are running. local repo_root - repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" CID="$(docker ps -q --filter "label=devcontainer.local_folder=$repo_root" 2>/dev/null | head -1 || true)" - if [ -z "$CID" ]; then - # Fallback: any running devcontainer whose local folder looks like a bucketeer checkout - CID="$(docker ps --filter "label=devcontainer.local_folder" \ - --format '{{.ID}} {{.Label "devcontainer.local_folder"}}' 2>/dev/null \ - | awk '$2 ~ /bucketeer/ {print $1; exit}' || true)" - fi if [ -n "$CID" ]; then MODE=local return @@ -98,11 +95,14 @@ status_report() { if minikube status >/dev/null 2>&1; then # Ignore transient states (Pending/ContainerCreating/Init) — batch CronJobs # constantly spawn short-lived pods and would make the count flap. - pods=$(kubectl get pods --no-headers 2>/dev/null) - total=$(echo "$pods" | grep -c . || true) - failing=$(echo "$pods" | grep -cE "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" || true) - echo "minikube: running ($total pods, $failing failing)" - [ "$failing" -gt 0 ] && echo "$pods" | grep -E "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" + if pods=$(kubectl get pods --no-headers 2>/dev/null); then + total=$(echo "$pods" | grep -c . || true) + failing=$(echo "$pods" | grep -cE "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" || true) + echo "minikube: running ($total pods, $failing failing)" + [ "$failing" -gt 0 ] && echo "$pods" | grep -E "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" + else + echo "minikube: running, but kubectl failed to list pods — check kubectl config (kubectl config current-context)" + fi else echo "minikube: NOT running (start with: make start-minikube — never minikube start)" fi diff --git a/manifests/datadog-agent.yaml b/manifests/datadog-agent.yaml new file mode 100644 index 0000000000..6b0dccd4c1 --- /dev/null +++ b/manifests/datadog-agent.yaml @@ -0,0 +1,20 @@ +kind: "DatadogAgent" +apiVersion: "datadoghq.com/v2alpha1" +metadata: + name: "datadog" + namespace: "datadog" +spec: + global: + clusterName: "your-cluster-name-here" + site: "ap1.datadoghq.com" + credentials: + apiSecret: + secretName: "datadog-secret" + keyName: "api-key" + tags: + - "env:local" + features: + clusterChecks: + enabled: true + orchestratorExplorer: + enabled: true From 9cb9185ee37662ce57cfbed548bfcd2611f09eb6 Mon Sep 17 00:00:00 2001 From: Hien Vu Date: Fri, 31 Jul 2026 13:45:27 +0900 Subject: [PATCH 3/6] chore: remove unrelated datadog-agent manifest from this branch manifests/datadog-agent.yaml was accidentally included in the previous commit; it is local work unrelated to this PR. Co-Authored-By: Claude Fable 5 --- manifests/datadog-agent.yaml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 manifests/datadog-agent.yaml diff --git a/manifests/datadog-agent.yaml b/manifests/datadog-agent.yaml deleted file mode 100644 index 6b0dccd4c1..0000000000 --- a/manifests/datadog-agent.yaml +++ /dev/null @@ -1,20 +0,0 @@ -kind: "DatadogAgent" -apiVersion: "datadoghq.com/v2alpha1" -metadata: - name: "datadog" - namespace: "datadog" -spec: - global: - clusterName: "your-cluster-name-here" - site: "ap1.datadoghq.com" - credentials: - apiSecret: - secretName: "datadog-secret" - keyName: "api-key" - tags: - - "env:local" - features: - clusterChecks: - enabled: true - orchestratorExplorer: - enabled: true From 19dbe51a418ca7b30e4512cc0d214f3e4514a0c7 Mon Sep 17 00:00:00 2001 From: Hien Vu Date: Mon, 3 Aug 2026 17:24:16 +0900 Subject: [PATCH 4/6] refactor: rename devc skills to devcontainer-* and split docs by audience Address review comments on PR #2741: - Rename skills for clarity: devc -> devcontainer-run, devc-generate -> devcontainer-generate, devc-deploy -> devcontainer-deploy; the wrapper script is now .claude/skills/devcontainer-run/scripts/exec.sh - Move human-facing environment knowledge into DEVELOPMENT.md (working with the dev container from the host, start-minikube exit-1 note, single-service redeploy flow) and cross-reference it from the skills, which keep only the Claude-oriented operational detail Co-Authored-By: Claude Fable 5 --- .claude/CLAUDE.md | 8 +++---- .../SKILL.md | 13 ++++++------ .../SKILL.md | 15 ++++++------- .../{devc => devcontainer-run}/SKILL.md | 21 ++++++++++++------- .../scripts/exec.sh} | 6 +++--- DEVELOPMENT.md | 21 +++++++++++++++++++ 6 files changed, 56 insertions(+), 28 deletions(-) rename .claude/skills/{devc-deploy => devcontainer-deploy}/SKILL.md (87%) rename .claude/skills/{devc-generate => devcontainer-generate}/SKILL.md (79%) rename .claude/skills/{devc => devcontainer-run}/SKILL.md (80%) rename .claude/skills/{devc/scripts/devc-exec.sh => devcontainer-run/scripts/exec.sh} (95%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0f30d359fd..e2b225d23e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -89,7 +89,7 @@ make mockgen # Regenerate only mocks (after changing interfaces that h - Run `make proto-all` after any `.proto` file change — this regenerates Go bindings and OpenAPI/Swagger specs. - Run `make mockgen` after changing any Go interface that has generated mocks in a `mock/` directory. - The generated files are committed to the repo. `protoc` v23.4 must be on PATH ahead of any other version for the version header in `.pb.go` files to stay at `v4.23.4`. -- Prefer running generation inside the dev container via the `devc-generate` skill — the container guarantees protoc v23.4, while a host version mismatch churns every generated file. +- Prefer running generation inside the dev container via the `devcontainer-generate` skill — the container guarantees protoc v23.4, while a host version mismatch churns every generated file. ### Local development @@ -97,9 +97,9 @@ make mockgen # Regenerate only mocks (after changing interfaces that h Development actions — proto generation, builds, deploys, kubectl/helm — should run **inside the Bucketeer dev container**, where tool versions are guaranteed (protoc v23.4, go-tools, minikube/helm/kubectl). Check for a running dev container first before running these on the host. Use the project skills: -- `devc` — detect the running dev container (local devcontainer or Codespace) and run any command inside it: `bash .claude/skills/devc/scripts/devc-exec.sh status` -- `devc-generate` — proto/mock generation inside the container -- `devc-deploy` — deploy to the minikube cluster inside the container +- `devcontainer-run` — detect the running dev container (local devcontainer or Codespace) and run any command inside it: `bash .claude/skills/devcontainer-run/scripts/exec.sh status` +- `devcontainer-generate` — proto/mock generation inside the container +- `devcontainer-deploy` — deploy to the minikube cluster inside the container ```bash make start-minikube # Inside the dev container. Always use this, not `minikube start` directly diff --git a/.claude/skills/devc-deploy/SKILL.md b/.claude/skills/devcontainer-deploy/SKILL.md similarity index 87% rename from .claude/skills/devc-deploy/SKILL.md rename to .claude/skills/devcontainer-deploy/SKILL.md index 21963d5ed8..988c62bc80 100644 --- a/.claude/skills/devc-deploy/SKILL.md +++ b/.claude/skills/devcontainer-deploy/SKILL.md @@ -1,23 +1,24 @@ --- -name: devc-deploy +name: devcontainer-deploy description: >- Build and deploy Bucketeer to the minikube cluster inside the dev container, or redeploy/restart a single service there. Use this whenever the user wants to deploy locally, run "make deploy-bucketeer", start minikube, get their code changes running in the dev cluster, restart a crashing pod, or says - "devc-deploy", "deploy to minikube", "redeploy the backend". Also use it to + "devcontainer-deploy", "deploy to minikube", "redeploy the backend". Also use it to check deployment health (pods not ready, gateway not responding) in the dev container environment. --- -# devc-deploy — deploy Bucketeer inside the dev container +# devcontainer-deploy — deploy Bucketeer inside the dev container Deployment target is the minikube cluster *inside* the dev container (helm -charts in `manifests/`), not the host docker-compose stack. All commands go -through the devc wrapper (see the `devc` skill): +charts in `manifests/`), not the host docker-compose stack. (Human-facing +docs for these flows: `DEVELOPMENT.md` § "Deploy Bucketeer".) All commands go +through the devcontainer-run wrapper (see the `devcontainer-run` skill): ```bash -DEVC="bash .claude/skills/devc/scripts/devc-exec.sh" +DEVC="bash .claude/skills/devcontainer-run/scripts/exec.sh" ``` ## 1. Preflight diff --git a/.claude/skills/devc-generate/SKILL.md b/.claude/skills/devcontainer-generate/SKILL.md similarity index 79% rename from .claude/skills/devc-generate/SKILL.md rename to .claude/skills/devcontainer-generate/SKILL.md index bf2a688881..7570d0fab7 100644 --- a/.claude/skills/devc-generate/SKILL.md +++ b/.claude/skills/devcontainer-generate/SKILL.md @@ -1,28 +1,29 @@ --- -name: devc-generate +name: devcontainer-generate description: >- Regenerate Bucketeer protobuf Go bindings, OpenAPI/Swagger specs, and gomock files inside the dev container, where protoc is guaranteed to be exactly v23.4. Use this whenever a .proto file changed, generated *.pb.go / *.pb.gw.go / swagger files need regenerating, a mocked Go interface changed (mockgen), or the user says "generate proto", "regen protos", "make - proto-all", "make mockgen", or "devc-generate". Prefer this over running + proto-all", "make mockgen", or "devcontainer-generate". Prefer this over running protoc or make proto-all on the host — a host protoc version mismatch silently rewrites every generated file's header. --- -# devc-generate — code generation inside the dev container +# devcontainer-generate — code generation inside the dev container Generated files are committed to the repo, and their headers record the protoc version (`protoc v4.23.4`). The dev container ships exactly protoc 23.4, so generation must happen there; a different host protoc churns every `.pb.go` -file and the PR becomes unreviewable. +file and the PR becomes unreviewable. (Human-facing background: +`DEVELOPMENT.md` § "Working with the dev container from the host".) -All commands below go through the devc wrapper (see the `devc` skill for how +All commands below go through the devcontainer-run wrapper (see the `devcontainer-run` skill for how detection works): ```bash -DEVC="bash .claude/skills/devc/scripts/devc-exec.sh" +DEVC="bash .claude/skills/devcontainer-run/scripts/exec.sh" ``` ## 1. Pick the right target @@ -59,4 +60,4 @@ error; don't force it without flagging the compatibility break to the user. In a codespace the regenerated files land in the codespace's clone, not the host repo. Commit/push from inside, or copy back with `gh codespace cp`. The -`status` subcommand of the devc script tells you which mode you're in. +`status` subcommand of the devcontainer-run script tells you which mode you're in. diff --git a/.claude/skills/devc/SKILL.md b/.claude/skills/devcontainer-run/SKILL.md similarity index 80% rename from .claude/skills/devc/SKILL.md rename to .claude/skills/devcontainer-run/SKILL.md index 61e4ae79cf..59973f34e7 100644 --- a/.claude/skills/devc/SKILL.md +++ b/.claude/skills/devcontainer-run/SKILL.md @@ -1,17 +1,17 @@ --- -name: devc +name: devcontainer-run description: >- Detect the running Bucketeer dev container (local VS Code devcontainer or GitHub Codespace) and run commands inside it. Use this whenever a task should run in the dev container environment — make targets, builds, tests, kubectl / helm / minikube commands, checking whether the container is up — or when the - user says "devc", "dev container", "devcontainer", "codespace", or "run this + user says "devcontainer-run", "devc", "dev container", "devcontainer", "codespace", or "run this inside the container". Also use it when a task needs tools the container guarantees but the host may lack (protoc 23.4, mockgen, protolock, helm, - kubectl, minikube). devc-generate and devc-deploy build on this skill. + kubectl, minikube). devcontainer-generate and devcontainer-deploy build on this skill. --- -# devc — run commands inside the Bucketeer dev container +# devcontainer-run — run commands inside the Bucketeer dev container The dev container is the canonical Bucketeer development environment: Ubuntu with docker-in-docker, minikube + helm + kubectl, protoc v23.4, and Go tooling in @@ -19,17 +19,22 @@ docker-in-docker, minikube + helm + kubectl, protoc v23.4, and Go tooling in non-login shells). The workspace is `/workspaces/bucketeer`, the user is `codespace` (passwordless sudo). +Human-facing documentation for this environment lives in `DEVELOPMENT.md` +("Working with the dev container from the host" and the Minikube sections); +this skill is the Claude-oriented operational version — when changing one, +keep the other in sync. + ## How to run anything inside it Always go through the wrapper script — it finds the container and sets up PATH: ```bash # Where is the container, and is the environment healthy? -bash .claude/skills/devc/scripts/devc-exec.sh status +bash .claude/skills/devcontainer-run/scripts/exec.sh status # Run any command in /workspaces/bucketeer inside the container -bash .claude/skills/devc/scripts/devc-exec.sh 'make build-api' -bash .claude/skills/devc/scripts/devc-exec.sh 'kubectl get pods' +bash .claude/skills/devcontainer-run/scripts/exec.sh 'make build-api' +bash .claude/skills/devcontainer-run/scripts/exec.sh 'kubectl get pods' ``` Detection order (the script handles all of this): @@ -57,7 +62,7 @@ and let them choose, because host tool versions (especially protoc) may differ. Bash timeout (600000) or `run_in_background`. - `dockerd` inside the container is started by the post-attach hook, but that only fires when an editor attaches. If `status` says it's not running: - `bash .claude/skills/devc/scripts/devc-exec.sh 'nohup sudo dockerd > /tmp/dockerd.log 2>&1 & sleep 5 && docker info > /dev/null && echo ok'` + `bash .claude/skills/devcontainer-run/scripts/exec.sh 'nohup sudo dockerd > /tmp/dockerd.log 2>&1 & sleep 5 && docker info > /dev/null && echo ok'` - minikube must be started with `make start-minikube`, never `minikube start` directly (the make target restores the cluster config and localenv services). Note: `make start-minikube` intentionally **exits 1 if minikube is already diff --git a/.claude/skills/devc/scripts/devc-exec.sh b/.claude/skills/devcontainer-run/scripts/exec.sh similarity index 95% rename from .claude/skills/devc/scripts/devc-exec.sh rename to .claude/skills/devcontainer-run/scripts/exec.sh index af0ee10cbc..46aca498f2 100755 --- a/.claude/skills/devc/scripts/devc-exec.sh +++ b/.claude/skills/devcontainer-run/scripts/exec.sh @@ -2,8 +2,8 @@ # Locate the Bucketeer dev container and run a command inside it. # # Usage: -# devc-exec.sh status Report where the dev container is and its health -# devc-exec.sh Run a command in /workspaces/bucketeer inside it +# exec.sh status Report where the dev container is and its health +# exec.sh Run a command in /workspaces/bucketeer inside it # # Exit codes: 0 success, 2 no dev container found, otherwise the command's exit code. set -euo pipefail @@ -24,7 +24,7 @@ detect() { fi # Case 2: local devcontainer (VS Code "Reopen in Container" / devcontainer CLI). - # Derive the repo root from this script's location (/.claude/skills/devc/scripts/) + # Derive the repo root from this script's location (/.claude/skills/devcontainer-run/scripts/) # so the exact label match works from any cwd — no fuzzy fallback that could pick # the wrong container when multiple checkouts are running. local repo_root diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b8171a115a..881b558cb3 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -24,6 +24,16 @@ container [here](https://docs.github.com/en/github/developing-online-with-codesp dev container) 4. Wait for the dev container to be ready +## Working with the dev container from the host + +If you edit code on the host (or drive the repo with tools running on the host), run environment-sensitive commands **inside the dev container**, not on the host: + +- **kubectl / helm**: the host's kubectl context may point at a completely different cluster (e.g. a real remote cluster) instead of the minikube instance inside the container. Always run cluster commands inside the container. +- **Code generation**: generated `.pb.go` files record the protoc version and must be produced with exactly protoc v23.4, which the dev container guarantees. A different host protoc rewrites the header of every generated file and makes the diff unreviewable. +- **Gateway health checks**: `web-gateway.bucketeer.io` / `api-gateway.bucketeer.io` are resolved by the *container's* `/etc/hosts` (pointing at `minikube ip`), so `curl` checks against those hosts only work inside the container. + +For Claude Code users, the project ships skills that automate this: `.claude/skills/devcontainer-run` detects the running dev container (local or Codespace) and runs commands inside it — `bash .claude/skills/devcontainer-run/scripts/exec.sh status` shows where it is and whether it's healthy. `devcontainer-generate` and `devcontainer-deploy` build on it for codegen and minikube deploys. + # Local Development Setup You can set up Bucketeer locally using one of two methods: @@ -57,6 +67,8 @@ make start-minikube **Note:** When you restart the Minikube cluster, you must use `make start-minikube` to start it. Do not use `minikube start` directly. +**Note:** `make start-minikube` intentionally exits with an error if minikube is already running. Check with `minikube status` first — an "already running" failure is not a problem to fix. + It will add 2 hosts to `/etc/hosts` that point to the minikube IP address: * `api-gateway.bucketeer.io` for API Gateway Service @@ -84,6 +96,15 @@ If you need to deploy a single service, you can do as follows. helm install backend manifests/bucketeer/charts/backend/ --values manifests/bucketeer/charts/backend/values.dev.yaml ``` +For faster iteration on a single service after a Go code change, rebuild and load the images, then restart only that deployment (`api`, `web`, `batch-server`, or `subscriber`): + +```shell +make build-go-embed +TAG=localenv make build-docker-images +TAG=localenv make minikube-load-images +kubectl rollout restart deployment web && kubectl rollout status deployment web +``` + **Note:** You can switch between data warehouses (MySQL, PostgreSQL, BigQuery) but remember to update the `values.dev.yaml` file to match the data warehouse you are using as the events persister and web service must use same event store service. **Note:** We use the `values.dev.yaml` file to override the default values in `values.yaml` file. From a6ce77a1d25ba60d4c933f3995400d957fba5b18 Mon Sep 17 00:00:00 2001 From: Hien Vu Date: Mon, 24 Aug 2026 12:10:23 +0900 Subject: [PATCH 5/6] Update SKILL.md --- .claude/skills/devcontainer-deploy/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/devcontainer-deploy/SKILL.md b/.claude/skills/devcontainer-deploy/SKILL.md index 988c62bc80..3ead198f16 100644 --- a/.claude/skills/devcontainer-deploy/SKILL.md +++ b/.claude/skills/devcontainer-deploy/SKILL.md @@ -70,7 +70,7 @@ manifests/bucketeer/values.dev.yaml`. ```bash $DEVC 'kubectl get pods' # everything Running/Completed, restarts not climbing -$DEVC 'curl -sk https://web-gateway.bucketeer.io/health' # must run INSIDE the container +$DEVC 'curl -sk https://api-gateway.bucketeer.io/health' # must run INSIDE the container ``` The `*.bucketeer.io` hosts entries live in the container's `/etc/hosts` From ab6feeb36f516feac73e1778b841e5c408e19c45 Mon Sep 17 00:00:00 2001 From: Hien Vu Date: Wed, 2 Sep 2026 18:48:49 +0900 Subject: [PATCH 6/6] fix(claude): pin dev-container skills to the minikube context Address review comments on PR #2741: - Name the kube context explicitly in every cluster command (kubectl --context minikube, helm --kube-context minikube) and warn in `status` when the active context is not minikube, so a stray `kubectl config use-context` cannot redirect a deploy at a real cluster. - Route the chart-level `helm upgrade` through $DEVC like every other deploy command instead of leaving it to run on the host. - Run the protoc-header spot-check through $DEVC: in codespace mode a bare grep reads the host clone and can falsely validate a stale file. - Refuse to guess between multiple available Bucketeer codespaces; exit 2 with the list and honour an explicit BUCKETEER_CODESPACE override. --- .claude/skills/devcontainer-deploy/SKILL.md | 21 ++++++---- .claude/skills/devcontainer-generate/SKILL.md | 4 +- .claude/skills/devcontainer-run/SKILL.md | 7 +++- .../skills/devcontainer-run/scripts/exec.sh | 42 ++++++++++++++++--- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/.claude/skills/devcontainer-deploy/SKILL.md b/.claude/skills/devcontainer-deploy/SKILL.md index 3ead198f16..e43bb9c7f0 100644 --- a/.claude/skills/devcontainer-deploy/SKILL.md +++ b/.claude/skills/devcontainer-deploy/SKILL.md @@ -58,25 +58,32 @@ For a code change to one service (e.g. backend): ```bash $DEVC 'make build-go-embed && TAG=localenv make build-docker-images && TAG=localenv make minikube-load-images' -$DEVC 'kubectl rollout restart deployment && kubectl rollout status deployment ' +$DEVC 'kubectl --context minikube rollout restart deployment && kubectl --context minikube rollout status deployment ' ``` The Bucketeer deployments are `api`, `web`, `batch-server`, and `subscriber` -(confirm with `$DEVC 'kubectl get deployments'`). Chart-level -changes instead: `helm upgrade bucketeer manifests/bucketeer/ --values -manifests/bucketeer/values.dev.yaml`. +(confirm with `$DEVC 'kubectl --context minikube get deployments'`). Chart-level +changes instead: + +```bash +$DEVC 'helm upgrade bucketeer manifests/bucketeer/ --kube-context minikube --values manifests/bucketeer/values.dev.yaml' +``` + +Always go through `$DEVC` and always name the context: a bare `helm`/`kubectl` +runs against whatever context is currently active, which on the host is often a +real cluster. `$DEVC status` warns when the active context is not `minikube`. ## 4. Verify ```bash -$DEVC 'kubectl get pods' # everything Running/Completed, restarts not climbing +$DEVC 'kubectl --context minikube get pods' # everything Running/Completed, restarts not climbing $DEVC 'curl -sk https://api-gateway.bucketeer.io/health' # must run INSIDE the container ``` The `*.bucketeer.io` hosts entries live in the container's `/etc/hosts` (pointed at `minikube ip`) — curl from the host proves nothing. For a failing -pod: `$DEVC 'kubectl logs deploy/ --tail=100'` and -`$DEVC 'kubectl describe pod '`. +pod: `$DEVC 'kubectl --context minikube logs deploy/ --tail=100'` and +`$DEVC 'kubectl --context minikube describe pod '`. ## Related dev-cluster chores diff --git a/.claude/skills/devcontainer-generate/SKILL.md b/.claude/skills/devcontainer-generate/SKILL.md index 7570d0fab7..bcd7a0a622 100644 --- a/.claude/skills/devcontainer-generate/SKILL.md +++ b/.claude/skills/devcontainer-generate/SKILL.md @@ -52,7 +52,9 @@ error; don't force it without flagging the compatibility break to the user. outputs. **A diff touching every `.pb.go` in the repo means a wrong protoc version — abort and check `$DEVC 'protoc --version'`.** - Spot-check one regenerated file's header still says `protoc v4.23.4`: - `grep -m1 "protoc " proto//.pb.go` + `$DEVC 'grep -m1 "protoc " proto//.pb.go'` — run it through + `$DEVC`, not bare: in codespace mode the host clone is a *different* checkout, + so a bare `grep` would validate a stale file that was never regenerated. - Build still compiles: `$DEVC 'make build-go'` (or the affected `make build-`), and `$DEVC 'make gofmt'` after any Go changes. diff --git a/.claude/skills/devcontainer-run/SKILL.md b/.claude/skills/devcontainer-run/SKILL.md index 59973f34e7..d693129bdb 100644 --- a/.claude/skills/devcontainer-run/SKILL.md +++ b/.claude/skills/devcontainer-run/SKILL.md @@ -42,7 +42,12 @@ Detection order (the script handles all of this): 2. Local devcontainer → `docker ps` filtered by label `devcontainer.local_folder=`, exec via `docker exec`. 3. GitHub Codespace → `gh codespace list` (needs the `codespace` auth scope), exec via `gh codespace ssh`. -Exit code 2 means no container was found; the script prints how to start one. +If more than one available Bucketeer codespace matches (a fork is also named +`bucketeer`, or you keep several), the script refuses to guess and exits 2 with +the list — pick one with `export BUCKETEER_CODESPACE=`. + +Exit code 2 means no container was found (or the codespace was ambiguous); the +script prints how to start one. Don't fall back to running the command on the host in that case — tell the user and let them choose, because host tool versions (especially protoc) may differ. diff --git a/.claude/skills/devcontainer-run/scripts/exec.sh b/.claude/skills/devcontainer-run/scripts/exec.sh index 46aca498f2..af0521091b 100755 --- a/.claude/skills/devcontainer-run/scripts/exec.sh +++ b/.claude/skills/devcontainer-run/scripts/exec.sh @@ -35,11 +35,36 @@ detect() { return fi - # Case 3: GitHub Codespace (requires `gh` with the codespace scope) - CODESPACE="$(gh codespace list --json name,repository,state \ + # Case 3: GitHub Codespace (requires `gh` with the codespace scope). + # Never silently pick one: a fork named `bucketeer`, or several codespaces on + # this repo, would otherwise run commands in the wrong clone/branch/cluster. + local candidates + candidates="$(gh codespace list --json name,repository,state \ -q '.[] | select((.repository | endswith("/bucketeer")) and .state == "Available") | .name' 2>/dev/null \ - | head -1 || true)" - if [ -n "$CODESPACE" ]; then + || true)" + if [ -n "${BUCKETEER_CODESPACE:-}" ]; then + if ! echo "$candidates" | grep -qx -- "$BUCKETEER_CODESPACE"; then + echo "BUCKETEER_CODESPACE='$BUCKETEER_CODESPACE' is not an available Bucketeer codespace." >&2 + echo "Available:" >&2 + echo "$candidates" | sed 's/^/ - /' >&2 + exit 2 + fi + CODESPACE="$BUCKETEER_CODESPACE" + MODE=codespace + return + fi + local count + count="$(echo "$candidates" | grep -c . || true)" + if [ "$count" -gt 1 ]; then + echo "Multiple available Bucketeer codespaces found — refusing to guess:" >&2 + echo "$candidates" | sed 's/^/ - /' >&2 + echo "" >&2 + echo "Pick one explicitly:" >&2 + echo " export BUCKETEER_CODESPACE=" >&2 + exit 2 + fi + if [ "$count" -eq 1 ]; then + CODESPACE="$candidates" MODE=codespace return fi @@ -93,15 +118,20 @@ status_report() { command -v mockgen >/dev/null && echo "go-tools: OK" || echo "go-tools: MISSING (run bash .devcontainer/setup.sh)" docker info >/dev/null 2>&1 && echo "dockerd: running" || echo "dockerd: NOT running (start with: nohup sudo dockerd > /tmp/dockerd.log 2>&1 &)" if minikube status >/dev/null 2>&1; then + # Always query the minikube context explicitly: the active context may point + # somewhere else entirely (a real GKE cluster), and reporting its pods here + # would be exactly the confusion this wrapper exists to prevent. + ctx=$(kubectl config current-context 2>/dev/null || echo unknown) + [ "$ctx" != "minikube" ] && echo "kube-context: WARNING active context is \"$ctx\", not minikube — bare kubectl/helm commands would hit that cluster; pass --context minikube / --kube-context minikube" # Ignore transient states (Pending/ContainerCreating/Init) — batch CronJobs # constantly spawn short-lived pods and would make the count flap. - if pods=$(kubectl get pods --no-headers 2>/dev/null); then + if pods=$(kubectl --context minikube get pods --no-headers 2>/dev/null); then total=$(echo "$pods" | grep -c . || true) failing=$(echo "$pods" | grep -cE "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" || true) echo "minikube: running ($total pods, $failing failing)" [ "$failing" -gt 0 ] && echo "$pods" | grep -E "CrashLoopBackOff|ImagePull|ErrImage|Error|OOMKilled|Evicted" else - echo "minikube: running, but kubectl failed to list pods — check kubectl config (kubectl config current-context)" + echo "minikube: running, but kubectl failed to list pods in the minikube context — check kubectl config (kubectl config get-contexts)" fi else echo "minikube: NOT running (start with: make start-minikube — never minikube start)"