Files
ai-memory/scripts/check-native-packaging.sh
T

230 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# CI-safe checks for the native Linux package assets.
#
# This script must not mutate host-level /usr, /etc, /var, users, groups, or
# services. It validates systemd/sysusers/tmpfiles behavior against a temporary
# alternate root and removes that root on exit.
set -euo pipefail
TMP_ROOT=""
log() {
printf '==> %s\n' "$*"
}
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
repo_root() {
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${script_dir}/.." && pwd
}
require_tool() {
command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}
copy_system_unit_dependency() {
local unit="$1"
local dest="$2/usr/lib/systemd/system/$unit"
local source_dir
for source_dir in /usr/lib/systemd/system /lib/systemd/system; do
if [ -f "${source_dir}/${unit}" ]; then
cp "${source_dir}/${unit}" "${dest}"
return 0
fi
done
}
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -Fq -- "$needle" "$file"; then
fail "${file} does not contain expected text: ${needle}"
fi
}
main() {
require_tool systemd-analyze
require_tool systemd-sysusers
require_tool systemd-tmpfiles
local repo tmpfiles_output
repo="$(repo_root)"
cd "${repo}"
log "Checking shell packaging syntax"
bash -n packaging/aur/PKGBUILD
bash -n packaging/aur/PKGBUILD-bin
bash -n packaging/aur/ai-memory.install
bash -n bin/ai-memory
bash -n scripts/test-native-arch-systemd-distrobox.sh
if command -v makepkg >/dev/null 2>&1 && [ "$(id -u)" != "0" ]; then
log "Checking AUR .SRCINFO generation"
(cd packaging/aur && makepkg --printsrcinfo -p PKGBUILD) >/dev/null
(cd packaging/aur && makepkg --printsrcinfo -p PKGBUILD-bin) >/dev/null
else
log "Skipping makepkg .SRCINFO check (makepkg unavailable or running as root)"
fi
if command -v rpmspec >/dev/null 2>&1; then
log "Checking RPM spec syntax"
rpmspec --parse --define '_arch x86_64' packaging/rpm/ai-memory.spec >/dev/null
else
log "Skipping RPM spec check (rpmspec unavailable)"
fi
TMP_ROOT="$(mktemp -d /tmp/ai-memory-native-root.XXXXXX)"
cleanup() {
if [ -n "${TMP_ROOT}" ]; then
rm -rf "${TMP_ROOT}"
fi
}
trap cleanup EXIT
log "Checking host-launch wrapper routing"
local fake_docker fake_compat_docker fake_native wrapper_log docker_log native_lines
fake_docker="${TMP_ROOT}/forbidden-docker"
fake_compat_docker="${TMP_ROOT}/fake-compat-docker"
fake_native="${TMP_ROOT}/fake-ai-memory"
wrapper_log="${TMP_ROOT}/wrapper.log"
docker_log="${TMP_ROOT}/docker.log"
printf '%s\n' '#!/usr/bin/env bash' 'exit 97' >"${fake_docker}"
printf '%s\n' '#!/usr/bin/env bash' 'printf '\''%s\n'\'' "$*" >>"${AI_MEMORY_DOCKER_TEST_LOG}"' >"${fake_compat_docker}"
printf '%s\n' '#!/usr/bin/env bash' 'printf '\''%s\n'\'' "$*" >>"${AI_MEMORY_WRAPPER_TEST_LOG}"' >"${fake_native}"
chmod 0755 "${fake_docker}" "${fake_compat_docker}" "${fake_native}"
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory run codex --yolo
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory show --json --no-scan
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory continue --workspace work --yolo
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory workstreams --limit 5 --json
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory rename-workstream --from typo-nmae --to refactor-db
AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory install-hooks --agent claude-code --capture-mode allowlist --apply
assert_contains "${wrapper_log}" "run codex --yolo"
assert_contains "${wrapper_log}" "show --json --no-scan"
assert_contains "${wrapper_log}" "continue --workspace work --yolo"
assert_contains "${wrapper_log}" "workstreams --limit 5 --json"
assert_contains "${wrapper_log}" "rename-workstream --from typo-nmae --to refactor-db"
assert_contains "${wrapper_log}" "install-hooks --agent claude-code --capture-mode allowlist --apply"
native_lines="$(wc -l <"${wrapper_log}")"
AI_MEMORY_DOCKER="${fake_compat_docker}" AI_MEMORY_NATIVE_BIN="${fake_native}" \
AI_MEMORY_HOOK_PLATFORM=posix AI_MEMORY_DOCKER_TEST_LOG="${docker_log}" \
AI_MEMORY_WRAPPER_TEST_LOG="${wrapper_log}" \
bin/ai-memory install-hooks --agent claude-code --apply
assert_contains "${docker_log}" "install-hooks --agent claude-code --apply"
test "$(wc -l <"${wrapper_log}")" = "${native_lines}" \
|| fail "an explicit posix hook install must retain the container/script path"
if AI_MEMORY_DOCKER="${fake_docker}" AI_MEMORY_NATIVE_BIN="$(pwd)/bin/ai-memory" \
bin/ai-memory run codex 2>/dev/null; then
fail "AI_MEMORY_NATIVE_BIN pointing to the wrapper must be refused instead of recursing"
fi
log "Creating temporary alternate root"
mkdir -p \
"${TMP_ROOT}/etc" \
"${TMP_ROOT}/etc/ai-memory" \
"${TMP_ROOT}/usr/bin" \
"${TMP_ROOT}/usr/lib/systemd/system" \
"${TMP_ROOT}/usr/lib/systemd/user" \
"${TMP_ROOT}/usr/lib/sysusers.d" \
"${TMP_ROOT}/usr/lib/tmpfiles.d" \
"${TMP_ROOT}/var/lib"
: >"${TMP_ROOT}/etc/passwd"
: >"${TMP_ROOT}/etc/group"
: >"${TMP_ROOT}/usr/bin/ai-memory"
chmod 0755 "${TMP_ROOT}/usr/bin/ai-memory"
cp crates/ai-memory-cli/templates/config.default.toml "${TMP_ROOT}/etc/ai-memory/config.toml"
cp packaging/env/ai-memory.env "${TMP_ROOT}/etc/ai-memory/env"
chmod 0640 "${TMP_ROOT}/etc/ai-memory/env"
cp packaging/systemd/ai-memory.service "${TMP_ROOT}/usr/lib/systemd/system/ai-memory.service"
cp packaging/systemd/ai-memory-user.service "${TMP_ROOT}/usr/lib/systemd/user/ai-memory.service"
cp packaging/sysusers/ai-memory.conf "${TMP_ROOT}/usr/lib/sysusers.d/ai-memory.conf"
cp packaging/tmpfiles/ai-memory.conf "${TMP_ROOT}/usr/lib/tmpfiles.d/ai-memory.conf"
for unit in \
sysinit.target \
basic.target \
multi-user.target \
network-online.target \
sockets.target \
timers.target \
paths.target \
slices.target \
shutdown.target \
remote-fs.target \
local-fs.target \
swap.target; do
copy_system_unit_dependency "$unit" "$TMP_ROOT" || true
done
log "Checking sysusers in alternate root"
systemd-sysusers --root="${TMP_ROOT}" "${TMP_ROOT}/usr/lib/sysusers.d/ai-memory.conf" >/dev/null
assert_contains "${TMP_ROOT}/etc/passwd" "ai-memory service user:/var/lib/ai-memory:/usr/bin/nologin"
assert_contains "${TMP_ROOT}/etc/group" "ai-memory"
log "Checking tmpfiles in alternate root"
if systemd-tmpfiles --help 2>&1 | grep -q -- '--dry-run'; then
tmpfiles_output="$(systemd-tmpfiles --root="${TMP_ROOT}" --create --dry-run "${TMP_ROOT}/usr/lib/tmpfiles.d/ai-memory.conf" 2>&1)"
case "${tmpfiles_output}" in
*"/var/lib/ai-memory"*) ;;
*) fail "tmpfiles dry-run did not plan /var/lib/ai-memory: ${tmpfiles_output}" ;;
esac
elif [ "$(id -u)" = "0" ]; then
systemd-tmpfiles --root="${TMP_ROOT}" --create "${TMP_ROOT}/usr/lib/tmpfiles.d/ai-memory.conf" >/dev/null
test -d "${TMP_ROOT}/var/lib/ai-memory" || fail "tmpfiles did not create /var/lib/ai-memory in alternate root"
test "$(stat -c '%a' "${TMP_ROOT}/var/lib/ai-memory")" = "750" || fail "tmpfiles created /var/lib/ai-memory with unexpected mode"
else
tmpfiles_output="$(systemd-tmpfiles --root="${TMP_ROOT}" --cat-config "${TMP_ROOT}/usr/lib/tmpfiles.d/ai-memory.conf" 2>&1)"
case "${tmpfiles_output}" in
*"/var/lib/ai-memory"*) ;;
*) fail "tmpfiles config parse did not include /var/lib/ai-memory: ${tmpfiles_output}" ;;
esac
fi
assert_contains packaging/tmpfiles/ai-memory.conf "d /var/lib/ai-memory 0750 ai-memory ai-memory -"
log "Checking systemd units in alternate root"
systemd-analyze --root="${TMP_ROOT}" verify ai-memory.service
# systemd-analyze cannot combine --user and --root on some distro versions.
# Copy the user unit into the system search path under a temporary name to
# still parse and validate the Service/Install directives with the same fake
# /usr/bin/ai-memory executable.
cp packaging/systemd/ai-memory-user.service \
"${TMP_ROOT}/usr/lib/systemd/system/ai-memory-user-parse-test.service"
systemd-analyze --root="${TMP_ROOT}" verify ai-memory-user-parse-test.service
log "Checking expected native paths and modes"
assert_contains packaging/systemd/ai-memory.service "--data-dir /var/lib/ai-memory"
assert_contains packaging/systemd/ai-memory.service "--config /etc/ai-memory/config.toml"
assert_contains packaging/systemd/ai-memory.service "EnvironmentFile=-/etc/ai-memory/env"
assert_contains packaging/systemd/ai-memory.service "StateDirectory=ai-memory"
assert_contains packaging/systemd/ai-memory.service "ReadWritePaths=/var/lib/ai-memory"
assert_contains packaging/systemd/ai-memory-user.service "--data-dir %h/.local/share/ai-memory"
assert_contains packaging/systemd/ai-memory-user.service "--config %h/.config/ai-memory/config.toml"
assert_contains packaging/systemd/ai-memory-user.service "EnvironmentFile=-%h/.config/ai-memory/env"
test "$(stat -c '%a' "${TMP_ROOT}/etc/ai-memory/env")" = "640"
log "Native packaging checks passed without touching host service paths"
}
main "$@"