From 86c0f35956ef5dfffbd671a2e4e045f677acd445 Mon Sep 17 00:00:00 2001 From: Tanner Date: Tue, 31 Mar 2026 18:23:12 -0400 Subject: [PATCH] feat: parameterize deployed resource names with name_prefix bundle variable Add a name_prefix variable to databricks.yml so each deployer gets unique resource names and avoids collisions in shared workspaces. The app, database instance, and volume name fields now use ${var.name_prefix}. The initial-setup skill gains a Personalization phase that auto-detects the user and writes a local .databricks/bundle/dev.yml override. deploy.sh resolves the app name dynamically from the bundle. Closes #89 Co-Authored-By: Claude Sonnet 4.6 --- .claude/skills/initial-setup/SKILL.md | 104 ++++++++++++++++----- .gitignore | 1 + app.example.yml | 9 ++ databricks.yml | 3 + openspec/changes/name-prefix/spec.md | 128 ++++++++++++++++++++++++++ resources/app.yml | 2 +- resources/lakebase.yml | 2 +- resources/volume.yml | 2 +- scripts/deploy.sh | 19 +++- tests/test_dab_bundle.py | 28 ++++++ 10 files changed, 267 insertions(+), 31 deletions(-) create mode 100644 openspec/changes/name-prefix/spec.md diff --git a/.claude/skills/initial-setup/SKILL.md b/.claude/skills/initial-setup/SKILL.md index 9c053ce..425d1a2 100644 --- a/.claude/skills/initial-setup/SKILL.md +++ b/.claude/skills/initial-setup/SKILL.md @@ -42,16 +42,69 @@ If any check fails, tell the user what to install/configure and STOP. --- -## Phase 2 — Databricks App Configuration (app.yaml) +## Phase 2 — Personalization (name_prefix) -### 2a. Check if app.yaml exists +All deployed resource names (app, database, volume) are parameterized with the `name_prefix` bundle variable. This phase auto-detects a good prefix to avoid collisions in shared workspaces. + +### 2a. Detect the current user + +Extract the short username from the authenticated identity: + +```bash +SHORT_USER=$(databricks auth describe 2>&1 | awk '/^User:/{print $2; exit}' | sed 's/@.*//') +echo "Detected user: $SHORT_USER" +``` + +### 2b. Check for existing app collisions + +Check whether an app named `-claw` already exists: + +```bash +databricks apps list 2>/dev/null | grep "${SHORT_USER}-claw" || echo "No collision found" +``` + +If there IS a collision, warn the user and ask them to choose a different prefix. + +### 2c. Write the local bundle override + +Suggest `name_prefix = ` (e.g. `tanner` from `tanner@company.com`). Confirm with the user, then write: + +```bash +mkdir -p .databricks/bundle +cat > .databricks/bundle/dev.yml < @@ -178,14 +231,14 @@ If the scope does not exist: databricks secrets create-scope ``` -### 4b. Check each secret exists +### 5b. Check each secret exists For each secret key, check if it exists (this returns metadata only, NOT the value): ``` databricks secrets list-secrets ``` -### 4c. Set missing secrets +### 5c. Set missing secrets For any missing secret, instruct the user to run: ``` @@ -202,9 +255,9 @@ Always use `databricks secrets put-secret` which handles input securely. --- -## Phase 5 — Deploy +## Phase 6 — Deploy -### 5a. Full deploy +### 6a. Full deploy Run the end-to-end deployment, takes around 5-10 minutes to start when the ap compute is off (install deps, validate bundle, deploy, run migrations, start app): @@ -218,18 +271,21 @@ If it fails, read the error output and help the user fix the issue. Common probl - Migration failure → Lakebase not yet provisioned (retry after a minute) - Start failure → app configuration issue in `app.yaml` -### 5b. Verify deployment +### 6b. Verify deployment -Check that the app is running: -``` -databricks apps get claw-app +Check that the app is running. First resolve the deployed app name from Phase 2 (it will be `-claw`, e.g. `tanner-claw`): + +```bash +APP_NAME=$(cat .databricks/bundle/dev.yml 2>/dev/null | python3 -c "import sys,yaml; d=yaml.safe_load(sys.stdin); print(d['variables']['name_prefix'])" 2>/dev/null || echo "claw") +APP_NAME="${APP_NAME}-claw" +databricks apps get "$APP_NAME" ``` The status should show the app is ACTIVE or RUNNING. Then tail the logs briefly to confirm startup: -``` -databricks apps get-logs claw-app --limit 50 +```bash +databricks apps get-logs "$APP_NAME" --limit 50 ``` Look for: @@ -241,9 +297,9 @@ If there are errors, read them and help the user fix the issue before proceeding --- -## Phase 6 — Smoke Test +## Phase 7 — Smoke Test -### 6a. Identify the DM channel +### 7a. Identify the DM channel From the deployment logs, the bot should have resolved its own user_id and opened a self-DM channel. Tell the user: @@ -254,12 +310,12 @@ Tell the user: 4. Wait 10-15 seconds (the poller runs every `POLL_INTERVAL_SECONDS`) 5. Claw should reply in a thread -### 6b. Confirm success +### 7b. Confirm success Ask the user to confirm they received a reply. If they did — setup is complete! If not, check: -- App logs for errors: `databricks apps get-logs claw-app --limit 100` +- App logs for errors: `databricks apps get-logs "$APP_NAME" --limit 100` - That the Slack bot is in the workspace - That the `SLACK_UC_CONNECTION` points to the right Slack bot token - That the `ANTHROPIC_BASE_URL` and `ANTHROPIC_MODEL` are correct for their AI Gateway @@ -270,7 +326,7 @@ If not, check: When all phases pass, summarize: - Workspace host -- App name: `claw-app` +- App name: `-claw` (as configured in Phase 2) - Slack UC connection name - Memory volume path - AI Gateway endpoint diff --git a/.gitignore b/.gitignore index 29d717c..04f2be5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ dist/ build/ .env +.databricks/ diff --git a/app.example.yml b/app.example.yml index 0dfbf93..21dc003 100644 --- a/app.example.yml +++ b/app.example.yml @@ -3,6 +3,15 @@ # Copy this file to app.yaml and fill in your values. # app.yaml is gitignored since it contains deployment-specific configuration. # +# Resource names (app, database, volume) are parameterized via the `name_prefix` +# bundle variable in databricks.yml (default: "claw"). To deploy with a custom +# prefix (e.g. to avoid collisions in a shared workspace): +# +# databricks bundle deploy --var name_prefix=tanner +# +# Or run the /initial-setup skill which auto-detects your username and writes +# a local .databricks/bundle/dev.yml override so you never need --var manually. +# # See https://docs.databricks.com/en/dev-tools/databricks-apps/app-runtime.html command: diff --git a/databricks.yml b/databricks.yml index c8fe6d5..5fb7082 100644 --- a/databricks.yml +++ b/databricks.yml @@ -2,6 +2,9 @@ bundle: name: claw variables: + name_prefix: + description: Prefix for all deployed resource names (avoids collisions in shared workspaces) + default: claw ai_gateway_endpoint: description: AI Gateway serving endpoint name default: databricks-claude-sonnet-4-6 diff --git a/openspec/changes/name-prefix/spec.md b/openspec/changes/name-prefix/spec.md new file mode 100644 index 0000000..63dcdc1 --- /dev/null +++ b/openspec/changes/name-prefix/spec.md @@ -0,0 +1,128 @@ +# OpenSpec: Parameterize deployed resource names with name_prefix + +Issue: #89 + +## Current State + +Hardcoded resource names that would collide in a shared workspace: + +| File | Field | Current Value | +|---|---|---| +| `resources/app.yml` | `apps.claw_app.name` | `claw-app` | +| `resources/lakebase.yml` | `database_instances.claw_db.name` | `claw` | +| `resources/volume.yml` | `volumes.claw_memory.name` | `claw_memory` | +| `scripts/deploy.sh` | `cmd_start` | `databricks bundle run claw_app` (uses YAML key, OK) | +| `scripts/deploy.sh` | `cmd_app_deploy` | `databricks apps deploy claw-app` (hardcoded deployed name) | +| `scripts/deploy.sh` | `cmd_stop` | `databricks apps stop claw-app` (hardcoded deployed name) | +| `SKILL.md` (initial-setup) | Phase 5b | `databricks apps get claw-app` (hardcoded) | + +## Changes + +### 1. `databricks.yml` — add `name_prefix` variable + +Add a new variable `name_prefix` with default `"claw"` to the `variables` block. + +```yaml +variables: + name_prefix: + description: Prefix for all deployed resource names (avoids collisions in shared workspaces) + default: claw +``` + +### 2. `resources/app.yml` — parameterize app name + +Change the `name` field (NOT the YAML key `claw_app`): + +```yaml +resources: + apps: + claw_app: + name: ${var.name_prefix}-claw +``` + +### 3. `resources/lakebase.yml` — parameterize database instance name + +Change the `name` field (NOT the YAML key `claw_db`): + +```yaml +resources: + database_instances: + claw_db: + name: ${var.name_prefix}-claw-db +``` + +### 4. `resources/volume.yml` — parameterize volume name + +Change the `name` field (NOT the YAML key `claw_memory`): + +```yaml +resources: + volumes: + claw_memory: + name: ${var.name_prefix}_claw_memory +``` + +Note: Volume names use underscores (UC naming convention), not hyphens. + +### 5. `.gitignore` — add `.databricks/` + +Append `.databricks/` so local bundle overrides are not committed. + +### 6. `scripts/deploy.sh` — remove hardcoded app names + +The `cmd_app_deploy` and `cmd_stop` functions hardcode `claw-app`. Since the app name is now dynamic via `${var.name_prefix}`, these commands must resolve the name from the bundle. + +- `cmd_start` already uses `databricks bundle run claw_app` which resolves via the bundle — no change needed. +- `cmd_app_deploy` and `cmd_stop` use `databricks apps deploy/stop claw-app` directly. Add a helper that reads the app name from the bundle output or derives it from the variable. + +Approach: use `databricks bundle validate --target "$TARGET" -o json` to extract the resolved app name, or accept an `APP_NAME` env var with fallback to `claw-app`. + +Simpler approach: use `databricks bundle run` for start (already done). For `app-deploy` and `stop`, derive the name: + +```bash +resolve_app_name() { + databricks bundle validate --target "$TARGET" -o json 2>/dev/null \ + | python3 -c "import sys,json; b=json.load(sys.stdin); print(list(b['resources']['apps'].values())[0]['name'])" \ + 2>/dev/null || echo "claw-app" +} +``` + +Alternatively, since `deploy.sh` is a convenience script and `bundle run`/`bundle deploy` handle resolution, we can note in comments that `app-deploy` and `stop` assume the default prefix. For now, use the resolve approach. + +### 7. `.claude/skills/initial-setup/SKILL.md` — add Personalization phase + +Insert a new "Phase 1.5" (renumber as Phase 2, shifting others) after Prerequisites: + +Phase: Personalization +1. Run `databricks auth describe` to extract short username (part before @) +2. Run `databricks apps list` to check for `-claw` collision +3. Suggest `name_prefix = ` (e.g. `tanner` from `tanner@company.com`) +4. Create `.databricks/bundle/dev.yml` with: + ```yaml + variables: + name_prefix: + ``` +5. Show the user what was written + +Also update Phase 5b (now Phase 6b) to use the resolved app name instead of hardcoded `claw-app`. + +### 8. `app.example.yml` — add documentation comment + +Add a comment at the top documenting the `--var name_prefix=` override. + +### 9. `tests/test_dab_bundle.py` — update tests + +- Add `test_variable_name_prefix` to `TestDatabricksYml` +- Update any tests in `TestAppYml` or `TestLakebaseYml` that check for hardcoded names (currently none do explicitly, but add tests that verify the `name` fields contain `var.name_prefix`) + +## Files Modified + +1. `databricks.yml` +2. `resources/app.yml` +3. `resources/lakebase.yml` +4. `resources/volume.yml` +5. `.gitignore` +6. `scripts/deploy.sh` +7. `.claude/skills/initial-setup/SKILL.md` +8. `app.example.yml` +9. `tests/test_dab_bundle.py` diff --git a/resources/app.yml b/resources/app.yml index 9e64402..c38cff8 100644 --- a/resources/app.yml +++ b/resources/app.yml @@ -1,7 +1,7 @@ resources: apps: claw_app: - name: claw-app + name: ${var.name_prefix}-claw description: Claw — Slack-native AI assistant powered by Databricks source_code_path: .. config: {} diff --git a/resources/lakebase.yml b/resources/lakebase.yml index cda917d..97ccbc8 100644 --- a/resources/lakebase.yml +++ b/resources/lakebase.yml @@ -1,5 +1,5 @@ resources: database_instances: claw_db: - name: claw + name: ${var.name_prefix}-claw-db capacity: CU_1 diff --git a/resources/volume.yml b/resources/volume.yml index 40929ca..4489ff9 100644 --- a/resources/volume.yml +++ b/resources/volume.yml @@ -3,7 +3,7 @@ resources: claw_memory: catalog_name: ${var.catalog_name} schema_name: default - name: claw_memory + name: ${var.name_prefix}_claw_memory volume_type: MANAGED grants: - principal: account users diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 53a2ec2..6fcbebb 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -57,9 +57,18 @@ cmd_start() { databricks bundle run claw_app --target "$TARGET" } +resolve_app_name() { + # Resolve the deployed app name from the bundle (respects name_prefix variable) + databricks bundle validate --target "$TARGET" -o json 2>/dev/null \ + | python3 -c "import sys,json; b=json.load(sys.stdin); print(list(b['resources']['apps'].values())[0]['name'])" \ + 2>/dev/null || echo "claw-claw" +} + cmd_app_deploy() { - echo "==> Deploying app source only (target=$TARGET)..." - databricks apps deploy claw-app --source-code-path . + local app_name + app_name="$(resolve_app_name)" + echo "==> Deploying app source only (app=$app_name, target=$TARGET)..." + databricks apps deploy "$app_name" --source-code-path . } cmd_full() { @@ -71,8 +80,10 @@ cmd_full() { } cmd_stop() { - echo "==> Stopping app..." - databricks apps stop claw-app + local app_name + app_name="$(resolve_app_name)" + echo "==> Stopping app ($app_name)..." + databricks apps stop "$app_name" } case "$COMMAND" in diff --git a/tests/test_dab_bundle.py b/tests/test_dab_bundle.py index 16c8e0a..f4af753 100644 --- a/tests/test_dab_bundle.py +++ b/tests/test_dab_bundle.py @@ -31,6 +31,12 @@ def test_resources_lakebase_yml_exists(self): def test_deploy_script_exists(self): assert (ROOT / "scripts" / "deploy.sh").is_file() + def test_gitignore_has_databricks_dir(self): + gitignore = (ROOT / ".gitignore").read_text() + assert ".databricks/" in gitignore, ( + ".databricks/ must be in .gitignore (local bundle overrides)" + ) + def test_deploy_script_is_executable(self): script = ROOT / "scripts" / "deploy.sh" assert script.is_file() @@ -68,6 +74,10 @@ def test_variable_slack_uc_connection(self): def test_variable_secret_scope(self): assert "secret_scope" in self.data["variables"] + def test_variable_name_prefix(self): + assert "name_prefix" in self.data["variables"] + assert self.data["variables"]["name_prefix"]["default"] == "claw" + def test_includes_resources(self): includes = self.data.get("include", []) assert any("resources" in str(i) for i in includes) @@ -146,6 +156,12 @@ def test_no_env_client_secret(self): names = [e["name"] for e in env] assert "DATABRICKS_CLIENT_SECRET" not in names + def test_app_name_uses_name_prefix_var(self): + app = list(self.data["resources"]["apps"].values())[0] + assert "var.name_prefix" in app["name"], ( + "App name must use ${var.name_prefix} for collision avoidance" + ) + def test_has_permissions_or_resources(self): app = list(self.data["resources"]["apps"].values())[0] assert "resources" in app or "permissions" in app @@ -175,6 +191,12 @@ def test_volume_has_catalog_ref(self): vol = list(self.data["resources"]["volumes"].values())[0] assert "var.catalog_name" in str(vol.get("catalog_name", "")) + def test_volume_name_uses_name_prefix_var(self): + vol = list(self.data["resources"]["volumes"].values())[0] + assert "var.name_prefix" in str(vol.get("name", "")), ( + "Volume name must use ${var.name_prefix} for collision avoidance" + ) + def test_volume_has_grants(self): vol = list(self.data["resources"]["volumes"].values())[0] assert "grants" in vol @@ -195,6 +217,12 @@ def _load(self): def test_has_resources(self): assert "resources" in self.data + def test_db_instance_name_uses_name_prefix_var(self): + db = list(self.data["resources"]["database_instances"].values())[0] + assert "var.name_prefix" in db["name"], ( + "Database instance name must use ${var.name_prefix} for collision avoidance" + ) + # ── scripts/deploy.sh content ────────────────────────────────────────────────