diff --git a/.agents/skills/dev-branch/SKILL.md b/.agents/skills/dev-branch/SKILL.md new file mode 100644 index 000000000..7668f5ebe --- /dev/null +++ b/.agents/skills/dev-branch/SKILL.md @@ -0,0 +1,106 @@ +--- +name: dev-branch +description: Parallel development using hack/dev-branch.sh to isolate worktrees and Kind test clusters per feature. +--- + +# Multi-Agent Parallel Development with `hack/dev-branch.sh` + +This skill provides workflow guidance for executing concurrent development tasks, +working on separate features within the `agent-substrate/substrate` repository. + +By isolating each feature inside its own **Git worktree** and **Kind Kubernetes +cluster**, multiple agents can build, test, and debug features in parallel +without step-on or port/cluster resource contention. + +--- + +## Architecture & Principles + +| Resource | Isolation Strategy | Purpose | +| :--- | :--- | :--- | +| **Source Code** | Git Worktree (`.worktrees/`) | Independent code branch & checkout per agent | +| **Kubernetes Cluster** | Kind Cluster (`kind-dev-`) | Dedicated cluster running `ate-system` per agent | +| **Container Registry** | Shared Docker Registry (`kind-registry:5001`) | Centralized image storage shared safely across clusters | +| **Environment Vars** | `.dev-env.sh` / `dev-branch.sh exec` | Auto-routes `kubectl` & E2E tests to the correct cluster | + +--- + +## Step-by-Step Workflow + +### 1. Provision Environment + +When starting a feature or delegating a task to a subagent, provision an +isolated environment: + +```bash +./hack/dev-branch.sh setup +``` + +- **Branch Name Format**: Use distinct, descriptive names (e.g., + `agent-router-fix`, `agent-valkey-backup`). +- **Path**: Created automatically under `.worktrees/`. + +### 2. Spawning Subagents (Parallel Execution) + +When spawning parallel subagents using `invoke_subagent`: +- Pass the target worktree path (`.worktrees/`) in the prompt. +- Instruct subagents to use `./hack/dev-branch.sh exec -- + ` for running commands against their specific environment. + +Example prompt fragment: + +> You are assigned to implement feature X in branch `agent-feature-x`. +> Work inside the directory `.worktrees/agent-feature-x`. +> Run tests using `./hack/dev-branch.sh exec agent-feature-x -- ./hack/run-e2e-kind.sh`. + +### 3. Executing Commands & Testing + +Instead of manually setting environment variables in every subshell, use `exec`: + +```bash +# Run unit tests inside the worktree +./hack/dev-branch.sh exec -- go test ./... + +# Run E2E tests against the branch's dedicated Kind cluster +./hack/dev-branch.sh exec -- ./hack/run-e2e-kind.sh +``` + +Alternatively, `eval` the environment in the subshell: +```bash +cd .worktrees/ +eval "$(/path/to/main/hack/dev-branch.sh env )" +./hack/run-e2e-kind.sh +``` + +### 4. Avoiding Resource & Port Conflicts + +- **Kubernetes Contexts**: Contexts are automatically isolated per cluster + (`kind-kind-dev-`). +- **Port Forwarding**: When port-forwarding services (e.g. `atenet-router`), + select dynamic host ports or unique ports per agent to avoid collisions: + + ```bash + # Use random available host port + kubectl --context kind-kind-dev- port-forward -n ate-system svc/atenet-router :80 + ``` + +### 5. Listing Active Environments + +Check all active parallel environments: + +```bash +./hack/dev-branch.sh list +``` + +### 6. Teardown + +When work on a feature is finished and merged/submitted: + +```bash +./hack/dev-branch.sh teardown +``` + +- Deletes the `kind-dev-` cluster. +- Removes the `.worktrees/` directory. +- Preserves the shared `kind-registry` container if other agents' Kind clusters + are still active. diff --git a/hack/delete-kind-cluster.sh b/hack/delete-kind-cluster.sh index fe4aec4ff..fb6d6deaf 100755 --- a/hack/delete-kind-cluster.sh +++ b/hack/delete-kind-cluster.sh @@ -40,8 +40,13 @@ fi if [ "${reg_exists}" == true ]; then reg_created_by="$(docker inspect --format '{{index .Config.Labels "created-by"}}' "${reg_name}" 2>/dev/null)" if [ "${reg_created_by}" == "agent-substrate" ]; then - echo "Deleting registry container '${reg_name}' (created by us)..." - docker rm -f "${reg_name}" || true + remaining_clusters="$("${ROOT}"/hack/kind.sh get clusters 2>/dev/null | grep -v 'No kind clusters found' | grep -v '^$' || true)" + if [ -z "${remaining_clusters}" ]; then + echo "No remaining Kind clusters found. Deleting registry container '${reg_name}'..." + docker rm -f "${reg_name}" || true + else + echo "Other Kind clusters are still running, keeping registry container '${reg_name}'." + fi else echo "Registry container '${reg_name}' was not created by us (${reg_created_by}), leaving it running." fi diff --git a/hack/dev-branch.sh b/hack/dev-branch.sh new file mode 100755 index 000000000..2d40210ae --- /dev/null +++ b/hack/dev-branch.sh @@ -0,0 +1,278 @@ +#!/usr/bin/env bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit -o nounset -o pipefail + +GIT_COMMON_DIR="$(git rev-parse --git-common-dir)" +MAIN_ROOT="$(cd "${GIT_COMMON_DIR}/.." && pwd)" +cd "${MAIN_ROOT}" + +sanitize_name() { + local raw_name="$1" + local cleaned + cleaned="$(echo "${raw_name}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g' | sed -E 's/^-+|-+$//g')" + if [[ -z "${cleaned}" ]]; then + cleaned="dev" + fi + echo "${cleaned}" +} + +show_help() { + cat < [args] + +Manage self-contained development environments (Git worktree + Kind cluster). + +Commands: + setup [worktree-path] + Creates a Git worktree, provisions an isolated Kind cluster, deploys the + Substrate control plane, and writes a .dev-env.sh file. + + teardown [worktree-path] + Deletes the isolated Kind cluster and removes the Git worktree. + + env + Outputs environment variable export statements for testing against the branch's + Kind cluster. Usage: eval "\$($0 env )" + + exec -- + Executes the command inside the branch's worktree directory with environment + variables set for its Kind cluster. + + list + Lists active Git worktrees and Kind clusters. + + help, -h, --help + Displays this help message. + +Examples: + $0 setup feature-router + $0 exec feature-router -- hack/run-e2e-kind.sh + $0 teardown feature-router +EOF +} + +setup_env() { + local branch="${1:-}" + local target_path="${2:-}" + + if [[ -z "${branch}" ]]; then + echo "Error: Branch name required for setup." >&2 + echo "Usage: $0 setup [worktree-path]" >&2 + exit 1 + fi + + local s_name + s_name="$(sanitize_name "${branch}")" + local cluster_name="kind-dev-${s_name}" + + if [[ -z "${target_path}" ]]; then + target_path="${MAIN_ROOT}/.worktrees/${s_name}" + fi + + echo "==> Setting up development environment for branch '${branch}'..." + echo " Worktree path: ${target_path}" + echo " Kind cluster: ${cluster_name}" + + mkdir -p "$(dirname "${target_path}")" + if [[ -d "${target_path}" ]]; then + echo " Worktree directory '${target_path}' already exists." + else + if git rev-parse --verify "${branch}" >/dev/null 2>&1; then + echo " Adding worktree for existing branch '${branch}'..." + git worktree add "${target_path}" "${branch}" + elif git rev-parse --verify "origin/${branch}" >/dev/null 2>&1; then + echo " Adding worktree tracking 'origin/${branch}'..." + git worktree add -b "${branch}" "${target_path}" "origin/${branch}" + else + echo " Creating new branch '${branch}' and adding worktree..." + git worktree add -b "${branch}" "${target_path}" HEAD + fi + fi + + echo "==> Provisioning Kind cluster '${cluster_name}'..." + KIND_CLUSTER_NAME="${cluster_name}" "${MAIN_ROOT}/hack/create-kind-cluster.sh" + + echo "==> Deploying Agent Substrate control plane to '${cluster_name}'..." + KIND_CLUSTER_NAME="${cluster_name}" "${MAIN_ROOT}/hack/install-ate-kind.sh" --deploy-ate-system + + echo "==> Writing environment configuration file '${target_path}/.dev-env.sh'..." + cat < "${target_path}/.dev-env.sh" +# Environment configuration for dev branch '${branch}' +export NO_DEV_ENV="true" +export KIND_CLUSTER_NAME="${cluster_name}" +export KUBECTL_CONTEXT="kind-${cluster_name}" +export KO_DOCKER_REPO="localhost:5001" +export BUCKET_NAME="ate-snapshots" +EOF + + echo "" + echo "=========================================================================" + echo "Development environment setup complete!" + echo "" + echo "To start working in this environment:" + echo " cd ${target_path}" + echo " eval \"\$(${MAIN_ROOT}/hack/dev-branch.sh env ${branch})\"" + echo " # or: source .dev-env.sh" + echo "" + echo "To run E2E tests against this isolated environment:" + echo " ${MAIN_ROOT}/hack/dev-branch.sh exec ${branch} -- ./hack/run-e2e-kind.sh" + echo "" + echo "To teardown this environment when finished:" + echo " ${MAIN_ROOT}/hack/dev-branch.sh teardown ${branch} ${target_path}" + echo "=========================================================================" +} + +teardown_env() { + local branch="${1:-}" + local target_path="${2:-}" + + if [[ -z "${branch}" ]]; then + echo "Error: Branch name required for teardown." >&2 + echo "Usage: $0 teardown [worktree-path]" >&2 + exit 1 + fi + + local s_name + s_name="$(sanitize_name "${branch}")" + local cluster_name="kind-dev-${s_name}" + + if [[ -z "${target_path}" ]]; then + target_path="${MAIN_ROOT}/.worktrees/${s_name}" + fi + + echo "==> Tearing down development environment for branch '${branch}'..." + + echo "==> Deleting Kind cluster '${cluster_name}'..." + KIND_CLUSTER_NAME="${cluster_name}" "${MAIN_ROOT}/hack/delete-kind-cluster.sh" || true + + if git worktree list | grep -q "${target_path}"; then + echo "==> Removing Git worktree at '${target_path}'..." + git worktree remove --force "${target_path}" || true + elif [[ -d "${target_path}" ]]; then + echo "==> Removing directory '${target_path}'..." + rm -rf "${target_path}" + fi + + echo "Teardown complete for '${branch}'." +} + +print_env() { + local branch="${1:-}" + + if [[ -z "${branch}" ]]; then + echo "# Error: Branch name required for env." >&2 + echo "# Usage: eval \"\$($0 env )\"" >&2 + exit 1 + fi + + local s_name + s_name="$(sanitize_name "${branch}")" + local cluster_name="kind-dev-${s_name}" + + cat <&2 + echo "Usage: $0 exec -- " >&2 + exit 1 + fi + shift + + if [[ "$#" -gt 0 && "$1" == "--" ]]; then + shift + fi + + if [[ "$#" -eq 0 ]]; then + echo "Error: No command specified to execute." >&2 + echo "Usage: $0 exec -- " >&2 + exit 1 + fi + + local s_name + s_name="$(sanitize_name "${branch}")" + local cluster_name="kind-dev-${s_name}" + local target_path="${MAIN_ROOT}/.worktrees/${s_name}" + + if [[ ! -d "${target_path}" ]]; then + echo "Error: Worktree directory '${target_path}' does not exist. Run setup first." >&2 + exit 1 + fi + + export NO_DEV_ENV="true" + export KIND_CLUSTER_NAME="${cluster_name}" + export KUBECTL_CONTEXT="kind-${cluster_name}" + export KO_DOCKER_REPO="localhost:5001" + export BUCKET_NAME="ate-snapshots" + + cd "${target_path}" + exec "$@" +} + +list_envs() { + echo "Active Git Worktrees:" + git worktree list + echo "" + echo "Active Kind Clusters:" + "${MAIN_ROOT}/hack/kind.sh" get clusters 2>/dev/null || echo "No kind clusters found." +} + +main() { + if [[ "$#" -eq 0 ]]; then + show_help + exit 0 + fi + + local cmd="$1" + shift + + case "${cmd}" in + setup|create) + setup_env "$@" + ;; + teardown|delete|destroy) + teardown_env "$@" + ;; + env) + print_env "$@" + ;; + exec) + exec_env "$@" + ;; + list|status) + list_envs "$@" + ;; + help|-h|--help) + show_help + ;; + *) + echo "Error: Unknown command '${cmd}'" >&2 + show_help + exit 1 + ;; + esac +} + +main "$@"