From be0cbeb4fb453c0751890e76c7101ce1642648f6 Mon Sep 17 00:00:00 2001 From: adamw Date: Thu, 17 Sep 2026 11:00:21 +0200 Subject: [PATCH] fix(cli): create shared cache volumes from devcontainer initializeCommand compose-all.yml declares sandcat-cache-* volumes as external. Only `sandcat run` created them, so an IDE's "Reopen in Container" on a fresh host failed with "external volume not found". A host-side script now creates them before compose runs. --- cli/templates/devcontainer/devcontainer.json | 4 ++ .../sandcat/scripts/ensure-cache-volumes.sh | 28 +++++++++ cli/test/init/devcontainer.bats | 5 ++ cli/test/init/ensure_cache_volumes.bats | 63 +++++++++++++++++++ docs/configuration/caches.md | 4 +- 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100755 cli/templates/devcontainer/sandcat/scripts/ensure-cache-volumes.sh create mode 100644 cli/test/init/ensure_cache_volumes.bats diff --git a/cli/templates/devcontainer/devcontainer.json b/cli/templates/devcontainer/devcontainer.json index 4f9bac46..37234ca0 100644 --- a/cli/templates/devcontainer/devcontainer.json +++ b/cli/templates/devcontainer/devcontainer.json @@ -11,6 +11,10 @@ // sandcat's security boundary. Matches the Dev Containers spec // default for dockerComposeFile scenarios. "overrideCommand": false, + // Shared cache volumes (sandcat-cache-*) are declared external in + // compose-all.yml. `sandcat run` creates them, but an IDE's "Reopen + // in Container" calls compose directly, so create them here first. + "initializeCommand": "bash ${localWorkspaceFolder}/.devcontainer/sandcat/scripts/ensure-cache-volumes.sh", // Remove credential sockets that VS Code forwards into the container // (SSH agent, git credential helper). Clearing env vars alone only // hides the paths — the socket files in /tmp can still be discovered diff --git a/cli/templates/devcontainer/sandcat/scripts/ensure-cache-volumes.sh b/cli/templates/devcontainer/sandcat/scripts/ensure-cache-volumes.sh new file mode 100755 index 00000000..504d6b93 --- /dev/null +++ b/cli/templates/devcontainer/sandcat/scripts/ensure-cache-volumes.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Creates the host-wide shared cache volumes (sandcat-cache-*) that +# compose-all.yml declares as `external: true`. Runs on the HOST as the +# devcontainer initializeCommand, so "Reopen in Container" from an IDE +# works on a machine where `sandcat run` has never created them. +# +# Idempotent: `docker volume create` on an existing name is a no-op. +# Silently does nothing when docker is missing so the IDE flow still +# gets compose's own, clearer error. +# +# Usage: ensure-cache-volumes.sh [compose-file] +# Default compose file: ../../compose-all.yml relative to this script. +set -euo pipefail + +compose_file=${1:-"$(dirname "$0")/../../compose-all.yml"} + +[ -f "$compose_file" ] || exit 0 +command -v docker >/dev/null 2>&1 || exit 0 + +# Only the `name:` lines of the external volume declarations match this +# prefix, so no YAML parser is needed on the host. grep exits 1 on no +# match, which is the normal case for projects without cache volumes. +names=$(grep -E '^[[:space:]]+name:[[:space:]]+sandcat-cache-' "$compose_file" | awk '{print $2}' || true) + +for name in $names; do + docker volume create --label sandcat-shared-cache=true "$name" >/dev/null +done diff --git a/cli/test/init/devcontainer.bats b/cli/test/init/devcontainer.bats index 7b6da7a8..d170b25e 100644 --- a/cli/test/init/devcontainer.bats +++ b/cli/test/init/devcontainer.bats @@ -35,6 +35,11 @@ teardown() { assert_success } +@test "devcontainer.json template runs ensure-cache-volumes.sh on the host before compose" { + run grep '"initializeCommand": "bash ${localWorkspaceFolder}/.devcontainer/sandcat/scripts/ensure-cache-volumes.sh"' "$DEVCONTAINER_JSON" + assert_success +} + @test "customize_devcontainer_json leaves no __PROJECT_NAME__ placeholders" { customize_devcontainer_json "$DEVCONTAINER_JSON" "my-project" diff --git a/cli/test/init/ensure_cache_volumes.bats b/cli/test/init/ensure_cache_volumes.bats new file mode 100644 index 00000000..480f2dc2 --- /dev/null +++ b/cli/test/init/ensure_cache_volumes.bats @@ -0,0 +1,63 @@ +#!/usr/bin/env bats + +setup() { + load test_helper + SCRIPT="$SCT_TEMPLATEDIR/devcontainer/sandcat/scripts/ensure-cache-volumes.sh" + COMPOSE_FILE="$BATS_TEST_TMPDIR/compose-all.yml" + cat > "$COMPOSE_FILE" <<'YAML' +services: + agent: + volumes: + - sandcat-cache-maven:/home/vscode/.m2/repository + - sandcat-cache-gradle:/home/vscode/.gradle/caches +volumes: + sandcat-cache-maven: + external: true + name: sandcat-cache-maven + sandcat-cache-gradle: + external: true + name: sandcat-cache-gradle + other-shared: + external: true + name: user-added-volume +YAML +} + +teardown() { + unstub_all +} + +@test "creates each sandcat-cache-* external volume with the shared-cache label" { + stub docker \ + "volume create --label sandcat-shared-cache=true sandcat-cache-maven : :" \ + "volume create --label sandcat-shared-cache=true sandcat-cache-gradle : :" + + run bash "$SCRIPT" "$COMPOSE_FILE" + assert_success + assert_output "" +} + +@test "does nothing when the compose file declares no cache volumes" { + echo "services: {}" > "$COMPOSE_FILE" + stub docker + + run bash "$SCRIPT" "$COMPOSE_FILE" + assert_success +} + +@test "exits 0 when the compose file is missing" { + run bash "$SCRIPT" "$BATS_TEST_TMPDIR/missing.yml" + assert_success +} + +@test "defaults to compose-all.yml two levels above the script" { + mkdir -p "$BATS_TEST_TMPDIR/.devcontainer/sandcat/scripts" + cp "$SCRIPT" "$BATS_TEST_TMPDIR/.devcontainer/sandcat/scripts/" + cp "$COMPOSE_FILE" "$BATS_TEST_TMPDIR/.devcontainer/compose-all.yml" + stub docker \ + "volume create --label sandcat-shared-cache=true sandcat-cache-maven : :" \ + "volume create --label sandcat-shared-cache=true sandcat-cache-gradle : :" + + run bash "$BATS_TEST_TMPDIR/.devcontainer/sandcat/scripts/ensure-cache-volumes.sh" + assert_success +} diff --git a/docs/configuration/caches.md b/docs/configuration/caches.md index fc0a8842..b86a043f 100644 --- a/docs/configuration/caches.md +++ b/docs/configuration/caches.md @@ -28,7 +28,9 @@ Each is a Docker named volume with a stable host-wide name projects reference the same physical volume, and `sandcat compose down -v` on one project will **not** wipe caches other projects rely on. The `sandcat run` wrapper creates them lazily via `docker volume create` (idempotent), so no -manual setup is required. +manual setup is required. The generated `devcontainer.json` also creates them +on the host via `initializeCommand` (`sandcat/scripts/ensure-cache-volumes.sh`), +so an IDE's "Reopen in Container" works without running `sandcat` first. Only `/home/vscode/.m2/repository/` is shared, not the whole `.m2/` — user config like `settings.xml` stays per-project inside `agent-home`. Same pattern