#!/usr/bin/env bash # Tears down the test stack for the current worktree and returns it to a # clean state: containers + volumes removed, test/.env, test/secrets/, and # test/testdata/ deleted. # # Safe to run when nothing is up. set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" TEST_DIR="${REPO_ROOT}/test" ENV_FILE="${TEST_DIR}/.env" COMPOSE_FILE="${TEST_DIR}/compose.yaml" # Compose only acts on services whose profiles are active, so a plain `down` # silently leaves every profiled service (e.g. the discourse group) running # along with its volumes. Enumerate the profiles the file declares rather than # hardcoding them, so a profile added later is torn down without editing this. env_file_args=() [[ -f "$ENV_FILE" ]] && env_file_args=(--env-file "$ENV_FILE") profile_args=() while IFS= read -r profile; do [[ -n "$profile" ]] && profile_args+=(--profile "$profile") done < <(docker compose -f "$COMPOSE_FILE" "${env_file_args[@]}" config --profiles 2>/dev/null || true) compose_down() { docker compose -f "$COMPOSE_FILE" "$@" "${profile_args[@]}" \ down -v --remove-orphans || true } if [[ -f "$ENV_FILE" ]]; then echo "Bringing down compose stack with volumes..." compose_down "${env_file_args[@]}" else # A .env deleted by hand, or a teardown that died partway, leaves containers # with nothing left to name their project. Recover the name from Docker's own # compose labels instead of re-deriving the bootstrap slug — that also catches # containers left by an earlier slug for this same directory. echo "No test/.env found; searching for stale containers from ${TEST_DIR}..." stale_projects=() while IFS= read -r project; do [[ -n "$project" ]] && stale_projects+=("$project") done < <(docker ps -aq \ --filter "label=com.docker.compose.project.working_dir=${TEST_DIR}" 2>/dev/null | xargs -r docker inspect \ --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null | sort -u) if [[ ${#stale_projects[@]} -eq 0 ]]; then echo " none found; skipping docker compose down." fi for project in "${stale_projects[@]}"; do echo "Bringing down stale project '${project}' with volumes..." compose_down --project-name "$project" done fi remove_path() { local path="$1" [[ -e "$path" ]] || return 0 echo "Removing ${path}" if rm -rf "$path" 2>/dev/null; then return 0 fi # Some testdata subdirs (postgres, temporal) are written by containers # running as root and end up owned by root on the host. Fall back to a # short-lived root container to delete them, avoiding a sudo prompt. echo " (root-owned files; removing via docker)" docker run --rm -v "${TEST_DIR}:/work" --workdir /work \ alpine:3 rm -rf "./$(basename "$path")" } # Determine whether this is the main worktree. Bootstrap copies test/secrets/ # from main into other worktrees, so deleting secrets/ on main wipes the # source-of-truth. Refuse to do that. MAIN_WT="$(git worktree list --porcelain | awk '/^worktree / { print $2; exit }')" IS_MAIN=0 if [[ "$REPO_ROOT" == "$MAIN_WT" ]]; then IS_MAIN=1 fi for path in \ "${TEST_DIR}/.env" \ "${TEST_DIR}/secrets"; do if [[ "$path" == "${TEST_DIR}/secrets" && "$IS_MAIN" -eq 1 ]]; then echo "Keeping ${path} (main worktree — source-of-truth for other worktrees)" continue fi remove_path "$path" done # Clean runtime data under testdata/ but preserve committed seed files # (e.g. testdata/fedwiki/config.json, testdata/keycloak/master-realm.json). # git clean -fdX removes only gitignored paths. if [[ -d "${TEST_DIR}/testdata" ]]; then echo "Cleaning gitignored runtime data under ${TEST_DIR}/testdata/" if ! git -C "$REPO_ROOT" clean -fdX -- test/testdata/ 2>/dev/null; then # Several services write their data dirs as root (postgres, temporal, # caddy, discourse's postgres), so the host user cannot unlink them. # Hand ownership back through a short-lived root container — generically, # over the whole tree — rather than enumerating per-service paths, which # is how gated services' data used to survive teardown. git clean then # does the selective delete: it, not this container, knows which paths # are runtime data and which are committed seed material. echo " (root-owned files; reclaiming ownership via docker)" docker run --rm -v "${TEST_DIR}:/work" --workdir /work alpine:3 \ chown -R "$(id -u):$(id -g)" testdata || true git -C "$REPO_ROOT" clean -fdX -- test/testdata/ 2>/dev/null || true fi fi echo "Teardown complete."