Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Use the dedicated CI helpers when you need to spin up the published stack inside
| `provision-qa` | Differential backup + targeted restore for QA databases. |
| `config-render` | Re-render `postgresql.conf` / `pg_hba.conf` from the templates and restart PostgreSQL (terminates active connections; required for some settings like `shared_buffers` and `max_connections`). |
| `config-check` | Compare live `postgresql.conf` / `pg_hba.conf` against rendered templates to catch drift. |
| `data-cleanup` | Report or remove stale `data/.pytest_backups` entries left by interrupted local pytest runs; dry-run by default, deletes entries older than 7 days only with `--execute`. |
| `audit-roles` / `audit-security` | Generate CSV/text reports covering role hygiene, passwords, and HBA/RLS posture. |
| `audit-extensions` | Confirm bundled extensions are present and on expected versions. |
| `audit-autovacuum` | Flag tables with high dead tuple counts or ratios. |
Expand Down Expand Up @@ -311,6 +312,8 @@ Use the dedicated CI helpers when you need to spin up the published stack inside

The CLI sources modular helpers from `scripts/lib/` so each function can be imported by tests or future automation.

Interrupted local test runs can leave full service-directory snapshots under `data/.pytest_backups/`. These are disposable pytest stashes, not live cluster state. Inspect them with `./scripts/manage.sh data-cleanup`; remove stale entries with `./scripts/manage.sh data-cleanup --execute`. The command only targets `.pytest_backups`, defaults to entries older than 7 days, and refuses to delete while Compose containers are running unless `--force` is supplied.

`daily-maintenance` now emits a richer bundle under `backups/daily/<YYYYMMDD>/`, including `pg_stat_statements` snapshots, `pg_buffercache` heatmaps, role/extension/autovacuum/replication CSVs, pg_cron schedules, pg_squeeze activity, and a security checklist alongside logs, dumps, pgBadger HTML, and pgaudit summaries. The workflow also records per-step results in `maintenance_status.json`, records the most recent sidecar dump run in `logical_backup_status.txt`, runs `partman.run_maintenance_proc()` across each database so freshly created partitions land even if the background worker interval has not elapsed, and captures version drift in `version_status.csv` (focusing on out-of-date components). Pair those reports with `config-check` to keep the rendered configs aligned with the templates. Tune the thresholds via `DAILY_PG_STAT_LIMIT`, `DAILY_BUFFERCACHE_LIMIT`, `DAILY_DEAD_TUPLE_THRESHOLD`, `DAILY_DEAD_TUPLE_RATIO`, and `DAILY_REPLICATION_LAG_THRESHOLD` as needed.

Nightly cron jobs also refresh pg_squeeze targets, reset `pg_stat_statements`, and run a safe `VACUUM (ANALYZE, SKIP_LOCKED, PARALLEL 4)` so statistics stay current without blocking hot tables.
Expand Down
7 changes: 7 additions & 0 deletions docs/SOURCE_DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Source Documentation Index

This index links repository source documentation that is required by the local
module documentation policy.

- [../tests/MODULE.md](../tests/MODULE.md) - test-suite contracts and cleanup
safety expectations.
11 changes: 11 additions & 0 deletions repo_config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
hooks:
enabled_groups:
- format
- syntax
- workflow
- go
- ai
- commit-msg

python:
docstring_coverage:
enabled: false
pytest_gate:
enabled: false
39 changes: 39 additions & 0 deletions scripts/lib/apparmor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Blackcat Informatics® Inc.
# SPDX-License-Identifier: MIT

# shellcheck shell=bash
set -euo pipefail

APPARMOR_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=common.sh
# shellcheck disable=SC1091
source "${APPARMOR_LIB_DIR}/common.sh"

cmd_apparmor_load() {
local parser=${APPARMOR_PARSER:-apparmor_parser}
if ! command -v "${parser}" >/dev/null 2>&1; then
echo "[apparmor] ${parser} not found. Install apparmor-utils (Debian/Ubuntu) or ensure apparmor_parser is on PATH." >&2
exit 1
fi
if [[ $EUID -ne 0 ]] && ! command -v sudo >/dev/null 2>&1; then
echo "[apparmor] sudo required to load profiles or rerun as root." >&2
exit 1
fi
local loaded=false
for profile in "${ROOT_DIR}/apparmor"/*.profile; do
[[ -e "${profile}" ]] || continue
if [[ $EUID -ne 0 ]]; then
sudo "${parser}" -r -W "${profile}" || exit 1
else
"${parser}" -r -W "${profile}" || exit 1
fi
loaded=true
echo "[apparmor] loaded ${profile##*/}" >&2
done
if [[ ${loaded} == false ]]; then
echo "[apparmor] no profiles found under ${ROOT_DIR}/apparmor" >&2
exit 1
fi
echo "[apparmor] profiles loaded. Set CORE_DATA_APPARMOR_<SERVICE>=apparmor:core_data_minimal (or your custom profile) before composing." >&2
}
195 changes: 195 additions & 0 deletions scripts/lib/data_cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Blackcat Informatics® Inc.
# SPDX-License-Identifier: MIT

# shellcheck shell=bash
set -euo pipefail

DATA_CLEANUP_LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=common.sh
# shellcheck disable=SC1091
source "${DATA_CLEANUP_LIB_DIR}/common.sh"

DATA_CLEANUP_DEFAULT_RETENTION=${DATA_CLEANUP_DEFAULT_RETENTION:-7d}
CORE_DATA_DATA_ROOT=${CORE_DATA_DATA_ROOT:-${ROOT_DIR}/data}

data_cleanup_usage() {
cat <<'USAGE'
Usage: manage.sh data-cleanup [options]

Remove stale pytest data stashes left under data/.pytest_backups.

Options:
--older-than AGE Retain entries newer than AGE (default: 7d).
AGE accepts s, m, h, or d suffixes.
--execute Delete matching entries. Without this, only report.
--force Allow execution even when compose containers are running.
--json Emit a JSON summary.
-h, --help Show this help.
USAGE
}

data_cleanup_parse_age() {
local age=$1
local number
local suffix

if [[ "${age}" =~ ^([0-9]+)([smhd])$ ]]; then
number=${BASH_REMATCH[1]}
suffix=${BASH_REMATCH[2]}
elif [[ "${age}" =~ ^([0-9]+)$ ]]; then
number=${BASH_REMATCH[1]}
suffix=d
else
echo "[data-cleanup] invalid age '${age}'; expected values like 24h or 7d." >&2
return 1
fi

case "${suffix}" in
s) echo "${number}" ;;
m) echo $((number * 60)) ;;
h) echo $((number * 60 * 60)) ;;
d) echo $((number * 24 * 60 * 60)) ;;
esac
}

data_cleanup_compose_running() {
local output
if ! output=$(compose ps -q 2>/dev/null); then
return 2
fi
[[ -n "${output}" ]]
}

data_cleanup_json_escape() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//$'\n'/\\n}
printf '%s' "${value}"
}
Comment on lines +64 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add escapes for tabs, carriage returns, and control characters.

The current implementation only escapes backslashes, quotes, and newlines. While this works for typical file paths, it may produce invalid JSON if paths contain tabs, carriage returns, or other control characters.

🔧 Proposed fix to handle additional escape sequences
 data_cleanup_json_escape() {
   local value=$1
   value=${value//\\/\\\\}
   value=${value//\"/\\\"}
+  value=${value//$'\t'/\\t}
+  value=${value//$'\r'/\\r}
+  value=${value//$'\f'/\\f}
   value=${value//$'\n'/\\n}
   printf '%s' "${value}"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data_cleanup_json_escape() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//$'\n'/\\n}
printf '%s' "${value}"
}
data_cleanup_json_escape() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//$'\t'/\\t}
value=${value//$'\r'/\\r}
value=${value//$'\f'/\\f}
value=${value//$'\n'/\\n}
printf '%s' "${value}"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/data_cleanup.sh` around lines 64 - 70, The
data_cleanup_json_escape function only handles backslashes, quotes and newlines;
update it to also escape tabs and carriage returns and to encode other control
characters (U+0000..U+001F) as JSON \uXXXX sequences so output is valid JSON.
Specifically, inside data_cleanup_json_escape add replacements for $'\t' -> \\t
and $'\r' -> \\r (operating on local value), and implement a fallback that scans
remaining bytes in value and replaces any byte <= 0x1F with a \uXXXX escape
(e.g., using a loop with printf and hex formatting) so non-printable control
characters are emitted as \\u00NN.


cmd_data_cleanup() {
local older_than=${DATA_CLEANUP_DEFAULT_RETENTION}
local execute=false
local force=false
local json=false

while [[ $# -gt 0 ]]; do
case "$1" in
--older-than)
if [[ $# -lt 2 ]]; then
echo "[data-cleanup] --older-than requires an age value." >&2
return 1
fi
older_than=$2
shift 2
;;
--older-than=*)
older_than=${1#*=}
shift
;;
--execute)
execute=true
shift
;;
--force)
force=true
shift
;;
--json)
json=true
shift
;;
-h | --help)
data_cleanup_usage
return 0
;;
*)
echo "[data-cleanup] unknown option: $1" >&2
data_cleanup_usage >&2
return 1
;;
esac
done

local retention_seconds
retention_seconds=$(data_cleanup_parse_age "${older_than}")
local now
now=$(date +%s)
local cutoff=$((now - retention_seconds))
local backup_root="${CORE_DATA_DATA_ROOT%/}/.pytest_backups"
local candidates=()
local candidate_count=0
local total_bytes=0

if [[ -d "${backup_root}" ]]; then
local path
while IFS= read -r -d '' path; do
local modified
modified=$(stat -c '%Y' "${path}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Search for platform detection patterns or stat usage

rg -nP '\bstat\s+-[cf]' --type=sh -C3
rg -nP '\buname\b.*Darwin|OSTYPE.*darwin' --type=sh -C3

Repository: paudley/core_data

Length of output: 1084


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Show the relevant section of data_cleanup.sh around the reported lines
sed -n '110,170p' scripts/lib/data_cleanup.sh | nl -ba | sed -n '1,120p'

# Find other GNU-specific du/stat usages and macOS fallbacks
rg -n --fixed-string "du -s -B" --type=sh
rg -n --fixed-string "stat -f '%m'" --type=sh
rg -nP "\bstat\s+-c\s+['\"]?%Y['\"]?" --type=sh
rg -nP "stat\s+-c\s+['\"]?%Y['\"]?|stat\s+-f\s+['\"]?%m['\"]?" --type=sh

Repository: paudley/core_data

Length of output: 104


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Show relevant section with line numbers (no nl dependency)
sed -n '110,170p' scripts/lib/data_cleanup.sh | cat -n

# Search for GNU-specific du/stat usage and any macOS fallbacks
rg -n --fixed-string "du -s -B" --type=sh .
rg -n --fixed-string "stat -f '%m'" --type=sh .
rg -nP "\bstat\s+-c\s+['\"]?%Y['\"]?" --type=sh .
rg -nP "stat\s+-c\s+['\"]?%Y['\"]?|stat\s+-f\s+['\"]?%m['\"]?" --type=sh .

Repository: paudley/core_data

Length of output: 2872


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Re-print the exact problematic block with line numbers
sed -n '120,150p' scripts/lib/data_cleanup.sh | cat -n

# Search repo for the same portability-sensitive commands
rg -n --fixed-strings "du -s -B1" --type=sh .
rg -nP "\bstat\s+-c\s+['\"]?%Y['\"]?" --type=sh .
rg -n --fixed-strings "stat -f '%m'" --type=sh .
rg -nP "stat\s+-c\s+['\"]?%Y['\"]?|stat\s+-f\s+['\"]?%m['\"]?" --type=sh .

Repository: paudley/core_data

Length of output: 1712


Fix macOS portability for stat/du in scripts/lib/data_cleanup.sh.

  • stat -c '%Y' (line 130) is GNU/BSD-incompatible; the repo already handles this elsewhere via stat -c ... || stat -f %m ... in scripts/daily_maintenance.sh, but data_cleanup.sh doesn’t.
  • du -s -B1 (line 133) is also GNU-specific and has no macOS-compatible fallback in the codebase.
Proposed fix (for `stat`)
     local path
     while IFS= read -r -d '' path; do
       local modified
-      modified=$(stat -c '%Y' "${path}")
+      if [[ "$(uname -s)" == "Darwin" ]]; then
+        modified=$(stat -f '%m' "${path}")
+      else
+        modified=$(stat -c '%Y' "${path}")
+      fi
       if ((modified <= cutoff)); then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/data_cleanup.sh` at line 130, Replace the GNU-only stat and du
invocations with portable fallbacks: for the modified timestamp assignment (the
code that sets the modified variable from stat), use "stat -c '%Y' \"${path}\"
|| stat -f %m \"${path}\"" to capture the file's mtime in a way that works on
both Linux and macOS; and for the directory size calculation (where du -s -B1 is
used), fall back to "du -s -B1 \"${path}\" || du -sk \"${path}\" | awk '{print
$1 * 1024}'" so you get bytes on GNU systems or convert kilobytes-to-bytes on
macOS — update the lines that set modified and the size_bytes (or equivalent)
variables accordingly.

if ((modified <= cutoff)); then
local bytes
bytes=$(du -s -B1 "${path}" | awk '{print $1}')
Comment on lines +128 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Search for du usage and platform detection

rg -nP '\bdu\s+.*-[Bk]' --type=sh -C3

Repository: paudley/core_data

Length of output: 566


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== du usage with -B/-b flags =="
rg -nP '\bdu\s+.*-(B|b)\b' . || true

echo
echo "== du usage with bytes/size flags =="
rg -nP '\bdu\s+-(s|k|K|-h|b|B)\b' . || true

echo
echo "== platform detection for Darwin/BSD =="
rg -nP 'Darwin|uname\s+-s|OSTYPE|sysctl\s+-n\s+hw\.model' --glob='*.sh' --glob='*.bash' --glob='*.zsh' --glob='*.inc' --glob='*.plugin' . || true

Repository: paudley/core_data

Length of output: 262


🌐 Web query:

macOS BSD du does not support -B flag

💡 Result:

The macOS du command is a BSD-based utility, which differs from the GNU Coreutils du commonly found on Linux [1][2]. The assertion that macOS du does not support the -B flag is incorrect; the flag is supported, but its function and behavior differ significantly from the GNU implementation [1][3]. In the BSD du version used on macOS, the -B blocksize option is used to calculate and display block counts based on a specified blocksize in bytes [4][1]. This is designed to estimate the space a file hierarchy would occupy on a filesystem with that specific block size [1][3]. This differs from GNU du, where -B (or --block-size) is primarily used to scale output units for display purposes [5][6]. Because the implementations are different, the two versions of du are not interchangeable in how they handle block size arguments [3]. If you are writing scripts intended to be portable between macOS and Linux, you should be aware that these flags do not perform the same task [3]. For simple output scaling on macOS, you may alternatively use the -k (KiB) or -m (MiB) flags, or set the BLOCKSIZE environment variable [1][7].

Citations:


🌐 Web query:

macOS BSD du -B blocksize meaning output units with -s

💡 Result:

On macOS, the -B blocksize option for the du command instructs it to calculate and display file system usage in units of the specified blocksize in bytes [1][2]. When used in conjunction with the -s (summary) option, the output will represent the total size of the specified file or directory expressed in those custom-sized blocks [1][2]. Important details regarding this behavior include: 1. Functionality: The -B blocksize option performs a calculation that estimates how much space the examined file hierarchy would occupy if it were stored on a file system using that specific block size [1][2]. 2. Precedence and Interaction: This option is distinct from other output flags like -h (human-readable), -k (kilobytes), -m (megabytes), or -g (gigabytes), as well as the BLOCKSIZE environment variable [1][2]. 3. Defaults: If neither -B nor other specific size options are provided, and the BLOCKSIZE environment variable is not set, macOS du defaults to displaying output in 512-byte blocks [3][4][2]. Note that macOS uses the BSD version of du, which differs from the GNU coreutils version found on many Linux distributions [4][5][6]. While both share the -B option, their internal implementations and the way they handle block-size calculations can vary [1][2][5].

Citations:


Adjust du -s -B1 portability expectation on macOS/BSD

du -s -B1 is not GNU-only—macOS/BSD du supports -B—so the current command likely won’t fail due to a missing flag. The main risk is differing du semantics/units between GNU and BSD, which can make “bytes” derived from -B1 less reliable. Prefer a portable approach like -k and convert to bytes.

🔧 Proposed fix (portable `du` size)
-        bytes=$(du -s -B1 "${path}" | awk '{print $1}')
+        bytes=$(du -sk "${path}" | awk '{print $1 * 1024}')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bytes=$(du -s -B1 "${path}" | awk '{print $1}')
bytes=$(du -sk "${path}" | awk '{print $1 * 1024}')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/data_cleanup.sh` at line 133, The current bytes calculation using
`du -s -B1 "${path}" | awk '{print $1}'` is brittle across GNU vs BSD du
semantics; update the bytes computation to a portable approach by using `du -sk
"${path}" | awk '{print $1*1024}'` (or use `stat` where available) so the script
assigns a consistent byte count to the `bytes` variable for the given `path`;
locate the line that sets `bytes` and replace the `du -s -B1` invocation with
the portable `du -sk` conversion (or an equivalent `stat` fallback) to ensure
correct units on macOS/BSD and Linux.

candidates+=("${path}")
candidate_count=$((candidate_count + 1))
total_bytes=$((total_bytes + bytes))
fi
Comment on lines +129 to +137

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of GNU-specific flags for stat (-c) and du (-B) will cause this command to fail on macOS, which is a common development environment. Additionally, since these directories often contain database files with restricted permissions, these commands might fail even on Linux if the user lacks read access to all subdirectories. With set -e and pipefail enabled, any such failure will crash the entire script. Consider providing fallbacks for portability and handling errors to prevent the script from terminating prematurely.

Suggested change
local modified
modified=$(stat -c '%Y' "${path}")
if ((modified <= cutoff)); then
local bytes
bytes=$(du -s -B1 "${path}" | awk '{print $1}')
candidates+=("${path}")
candidate_count=$((candidate_count + 1))
total_bytes=$((total_bytes + bytes))
fi
while IFS= read -r -d '' path; do
local modified
# Attempt GNU stat, fallback to BSD/macOS stat
if ! modified=$(stat -c '%Y' "${path}" 2>/dev/null); then
modified=$(stat -f '%m' "${path}" 2>/dev/null || echo 0)
fi
if ((modified > 0 && modified <= cutoff)); then
local bytes
# Attempt GNU du with bytes, fallback to du -k (KB) and convert to bytes
if ! bytes=$(du -s -B1 "${path}" 2>/dev/null | awk '{print $1}'); then
bytes=$(du -sk "${path}" 2>/dev/null | awk '{print $1 * 1024}' || echo 0)
fi
candidates+=("${path}")
candidate_count=$((candidate_count + 1))
total_bytes=$((total_bytes + bytes))
fi
done < <(find "${backup_root}" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)

done < <(find "${backup_root}" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
fi

if [[ "${execute}" == "true" && "${force}" != "true" ]]; then
local compose_state=0
data_cleanup_compose_running || compose_state=$?
case "${compose_state}" in
0)
echo "[data-cleanup] refusing to delete while compose containers are running; rerun after shutdown or pass --force." >&2
return 1
;;
2)
echo "[data-cleanup] unable to determine compose state; pass --force to execute anyway." >&2
return 1
;;
esac
fi

if [[ "${json}" == "true" ]]; then
printf '{"mode":"%s","backup_root":"%s","older_than":"%s","candidates":%d,"bytes":%d,"paths":[' \
"$([[ "${execute}" == "true" ]] && echo execute || echo dry-run)" \
"$(data_cleanup_json_escape "${backup_root}")" \
"$(data_cleanup_json_escape "${older_than}")" \
"${candidate_count}" \
"${total_bytes}"
local first=true
local candidate
for candidate in "${candidates[@]}"; do
if [[ "${first}" == "true" ]]; then
first=false
else
printf ','
fi
printf '"%s"' "$(data_cleanup_json_escape "${candidate}")"
done
printf ']}\n'
else
printf '[data-cleanup] mode: %s\n' "$([[ "${execute}" == "true" ]] && echo execute || echo dry-run)"
printf '[data-cleanup] backup root: %s\n' "${backup_root}"
printf '[data-cleanup] retention: older than %s\n' "${older_than}"
printf '[data-cleanup] candidates: %d\n' "${candidate_count}"
printf '[data-cleanup] reclaimable bytes: %d\n' "${total_bytes}"
local candidate
for candidate in "${candidates[@]}"; do
printf '%s\n' "${candidate}"
done
if [[ "${execute}" != "true" ]]; then
printf '[data-cleanup] dry run only; pass --execute to delete matching entries.\n'
fi
fi

if [[ "${execute}" == "true" ]]; then
local candidate
for candidate in "${candidates[@]}"; do
rm -rf -- "${candidate}"
done
Comment on lines +191 to +193

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If rm -rf fails for a specific candidate (e.g., due to permission issues), the script will terminate immediately because of set -e. It would be more resilient to log a warning and continue attempting to remove other stale candidates.

Suggested change
for candidate in "${candidates[@]}"; do
rm -rf -- "${candidate}"
done
for candidate in "${candidates[@]}"; do
rm -rf -- "${candidate}" || echo "[data-cleanup] warning: failed to remove ${candidate}" >&2
done

fi
}
36 changes: 8 additions & 28 deletions scripts/manage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ source "${SCRIPT_DIR}/lib/memcached.sh"
source "${SCRIPT_DIR}/lib/rabbitmq.sh"
# shellcheck source=scripts/lib/seccomp.sh
source "${SCRIPT_DIR}/lib/seccomp.sh"
# shellcheck source=scripts/lib/apparmor.sh
source "${SCRIPT_DIR}/lib/apparmor.sh"
# shellcheck source=scripts/lib/test_dataset.sh
source "${SCRIPT_DIR}/lib/test_dataset.sh"
# shellcheck source=scripts/lib/bootstrap_ci.sh
source "${SCRIPT_DIR}/lib/bootstrap_ci.sh"
# shellcheck source=scripts/lib/ci.sh
source "${SCRIPT_DIR}/lib/ci.sh"
# shellcheck source=scripts/lib/data_cleanup.sh
source "${SCRIPT_DIR}/lib/data_cleanup.sh"
# shellcheck source=scripts/lib/permissions.sh
source "${SCRIPT_DIR}/lib/permissions.sh"

Expand Down Expand Up @@ -172,6 +176,7 @@ Lifecycle
networks-show Print the currently rendered allow list.
config-render Re-render postgresql.conf/pg_hba.conf then restart PostgreSQL.
config-check Compare live configs to rendered templates.
data-cleanup Remove stale pytest data stashes (dry-run by default).
logs Tail postgres logs.
status Show container status and health.
service-urls Print connection URLs for local services using external host IP.
Expand Down Expand Up @@ -377,34 +382,6 @@ cmd_service_urls() {

}

cmd_apparmor_load() {
local parser=${APPARMOR_PARSER:-apparmor_parser}
if ! command -v "${parser}" >/dev/null 2>&1; then
echo "[apparmor] ${parser} not found. Install apparmor-utils (Debian/Ubuntu) or ensure apparmor_parser is on PATH." >&2
exit 1
fi
if [[ $EUID -ne 0 ]] && ! command -v sudo >/dev/null 2>&1; then
echo "[apparmor] sudo required to load profiles or rerun as root." >&2
exit 1
fi
local loaded=false
for profile in "${ROOT_DIR}/apparmor"/*.profile; do
[[ -e "${profile}" ]] || continue
if [[ $EUID -ne 0 ]]; then
sudo "${parser}" -r -W "${profile}" || exit 1
else
"${parser}" -r -W "${profile}" || exit 1
fi
loaded=true
echo "[apparmor] loaded ${profile##*/}" >&2
done
if [[ ${loaded} == false ]]; then
echo "[apparmor] no profiles found under ${ROOT_DIR}/apparmor" >&2
exit 1
fi
echo "[apparmor] profiles loaded. Set CORE_DATA_APPARMOR_<SERVICE>=apparmor:core_data_minimal (or your custom profile) before composing." >&2
}

ensure_compose

COMMAND=${CORE_DATA_SELECTED_COMMAND:-help}
Expand All @@ -429,6 +406,9 @@ ci-up)
ci-down)
cmd_ci_down "$@"
;;
data-cleanup)
cmd_data_cleanup "$@"
;;
build-image)
ensure_env
build_postgres_image
Expand Down
10 changes: 10 additions & 0 deletions tests/MODULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Tests Module

The `tests` package verifies Core Data management behavior from the operator
boundary. Lightweight tests avoid Docker where possible, while integration tests
exercise Compose-managed services and persistent data workflows.

Tests that create temporary service data must isolate their data roots and must
not remove live directories outside their fixture-owned paths. Cleanup tests use
fake Compose binaries to validate command safety gates without depending on a
running container stack.
13 changes: 13 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2026 Blackcat Informatics® Inc.
# SPDX-License-Identifier: MIT

"""Repository test suite contracts.

This package contains lightweight and integration tests for the Core Data
management tooling. The tests exercise operator-facing shell commands and
container workflows from outside the production code paths.

See Also:
MODULE.md: Test-suite contract and cleanup safety expectations.

"""
Loading
Loading