yams/scripts/local-ci/package-validate.sh
Trevon d398556ee5
fix(tests): replace frozen expectations and harden release gates (#323)
* test(search,daemon,storage): assert behaviour instead of frozen literals

Several suites encoded exact internal values that legitimate tuning and
ranking changes are allowed to move: SCIENTIFIC-era profile weights and
rrfK values, corpus-statistic figures, and hardcoded timeout/cancellation
numbers. They failed on every platform after those changes landed, so the
real behaviour regressions they should catch were hidden behind noise.

Replace the frozen literals with contract assertions: weights are finite,
non-negative and normalize, state selection still holds, capability-gated
sources stay zeroed, and hybrid results keep matching canonical hash/path
sets. Deadlines are not widened and the daemon paths they cover are
unchanged.

Also add independent corpus and adapter oracles, deterministic collector
cancellation and late-result gates, and synchronize ServiceManager
shutdown observation, so the tests fail for the behaviour they name.

The portability allowlist keeps its two reviewed raw test-env reads; only
their line numbers moved.

Signed-off-by: trvon <git@trevon.dev>

* ci(tests): summarise Meson failures without flooding the log

A failing test run printed the whole Meson suite output, so the actual
failing test names and their assertion messages were buried and the
uploaded logs were dominated by passing-test chatter.

Add a bounded reporter that extracts the failing test names plus their
surrounding diagnostics, keeps an overall cap on emitted lines, and is
unit-tested against representative Meson output. Wire it into the Tests
workflow so failures stay readable.

Signed-off-by: trvon <git@trevon.dev>

* build(deb): validate candidate identity, mapping and upgrade ordering

The Debian packaging scripts took filenames and versions on trust, so a
misnamed candidate or a wrong package ordering could pass validation while
the real install path was never exercised.

Fail closed on candidate identity, version and architecture, verify the
package-to-source mapping, seed the legacy-only layout, wait on the actual
services, and assert install/upgrade/remove ordering for both Debian and
RPM in an offline container. Cases that cannot be proven on a host are
reported as skipped rather than silently passing.

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>
2026-10-19 19:16:57 -06:00

340 lines
14 KiB
Bash
Executable file

#!/usr/bin/env bash
# yams/scripts/local-ci/package-validate.sh
#
# Install-validate the YAMS Linux packages (.deb, .rpm, Arch .pkg.tar.zst) the
# way a user would, in clean distro containers that boot systemd as PID 1.
# Single entry point for local runs (grim, any Linux box or Docker Desktop VM
# with Docker) and the release workflow's publish gate.
#
# The packages ship a systemd *user* unit. Per distro lane, in a fresh container:
# install - package manager install, no env vars, no manual steps
# layout - installed file list against a runtime allowlist; no headers,
# static libs, pkg-config or CMake files; no world-writable,
# setuid or non-root files; every ELF's DT_NEEDED resolves and
# RPATH/RUNPATH is $ORIGIN or /usr/lib[/yams] only
# service - user unit + user preset installed, enabled for all users
# (systemctl --global), no system unit, `systemd-analyze --user
# verify` clean, home and /tmp readable by the daemon
# user - a normal user with lingering: the unit is enabled and active
# without a manual start, socket in $XDG_RUNTIME_DIR, the CLI
# (no YAMS_* variables) adds from $HOME and /tmp, searches, uses
# ~/.local/share/yams, runs no second daemon; `yams daemon
# stop|restart` and CLI auto-start go through systemd; `daemon
# install --user` enables the packaged unit; users are isolated
# onnx - after `yams init`, the user daemon loads the onnx plugin with
# the bundled ONNX Runtime
# persist - container reboot: the user's unit is back, data still there
# remove - running user daemons stopped, global enablement removed, user
# corpora kept (deb: also purge)
# minimal - a second container from the bare base image (no systemd, no
# lane tooling) installs the package with declared deps only
# With --upgrade-from-<fmt>, a second container installs that older package,
# seeds and hydrates a user document, then verifies it after the transition.
# Legacy system-service lanes additionally record the quiesced /var/lib/yams
# files and verify their hashes; per-user lanes need no invented legacy path.
#
# Checks never stop at the first failure: each lane prints PASS/FAIL lines and
# a summary, and the script exits non-zero if any check failed.
#
# Usage:
# package-validate.sh [--only LANES] [--deb F] [--rpm F] [--arch F]
# [--upgrade-from-deb F] [--upgrade-from-rpm F]
# [--upgrade-from-arch F] [--build-dir DIR]
# [--report FILE] [--keep]
#
# LANES: debian, ubuntu, fedora, arch, or groups: deb (debian,ubuntu),
# rpm (fedora), linux (deb+rpm, default), all (linux+arch); comma-separated.
# Packages not given are discovered under --build-dir (default build/release).
#
# Examples:
# bash scripts/local-ci/package-validate.sh --only all \
# --deb yams-0.21.0-linux-x86_64.deb --rpm yams-0.21.0-linux-x86_64.rpm \
# --arch yams-0.21.0-1-x86_64.pkg.tar.zst \
# --upgrade-from-deb yams-0.20.3-linux-x86_64.deb
#
# Environment:
# ARCH_DOCKER_PLATFORM / ARCH_BASE_IMAGE Arch lane platform and base image
# UBUNTU_IMAGE (ubuntu:24.04) DEBIAN_IMAGE (debian:trixie-slim)
# FEDORA_IMAGE (fedora:42)
#
# Containers need --privileged and a private cgroup namespace (cgroup v2) so
# systemd can run units with their sandboxing; nothing touches host cgroups.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
CHECKS="${SCRIPT_DIR}/package-validate-checks.sh"
SUBSTRATE_DIR="${REPO_ROOT}/packaging/systemd"
ONLY="linux"
BUILD_DIR="${REPO_ROOT}/build/release"
DEB_PKG="" RPM_PKG="" ARCH_PKG=""
UP_DEB="" UP_RPM="" UP_ARCH=""
REPORT=""
KEEP=0
log() { printf '\033[1;34m[validate]\033[0m %s\n' "$*"; }
okmsg() { printf '\033[1;32m[ ok ]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; }
usage() { sed -n '2,59p' "${BASH_SOURCE[0]}"; }
while [ "$#" -gt 0 ]; do
case "$1" in
--only) ONLY="$2"; shift 2 ;;
--build-dir) BUILD_DIR="$2"; shift 2 ;;
--deb) DEB_PKG="$2"; shift 2 ;;
--rpm) RPM_PKG="$2"; shift 2 ;;
--arch) ARCH_PKG="$2"; shift 2 ;;
--upgrade-from-deb) UP_DEB="$2"; shift 2 ;;
--upgrade-from-rpm) UP_RPM="$2"; shift 2 ;;
--upgrade-from-arch) UP_ARCH="$2"; shift 2 ;;
--report) REPORT="$2"; shift 2 ;;
--keep) KEEP=1; shift ;;
-h | --help) usage; exit 0 ;;
*) err "unknown argument: $1"; usage; exit 2 ;;
esac
done
# Expand lane groups into an ordered, de-duplicated lane list.
LANES=()
add_lane() { case " ${LANES[*]-} " in *" $1 "*) ;; *) LANES+=("$1") ;; esac; }
IFS=',' read -r -a requested <<<"${ONLY}"
for item in "${requested[@]}"; do
case "${item}" in
all) add_lane debian; add_lane ubuntu; add_lane fedora; add_lane arch ;;
linux) add_lane debian; add_lane ubuntu; add_lane fedora ;;
deb) add_lane debian; add_lane ubuntu ;;
rpm) add_lane fedora ;;
debian | ubuntu | fedora | arch) add_lane "${item}" ;;
*) err "unsupported lane: ${item}"; exit 2 ;;
esac
done
[ "${#LANES[@]}" -gt 0 ] || { err "no lanes selected"; exit 2; }
command -v docker >/dev/null 2>&1 || { err "docker not found on PATH"; exit 2; }
[ -f "${CHECKS}" ] || { err "missing ${CHECKS}"; exit 2; }
discover_pkg() {
[ -d "${BUILD_DIR}" ] || return 0
{ find "${BUILD_DIR}" -maxdepth 4 -type f -name "$1" 2>/dev/null || true; } | LC_ALL=C sort | tail -n1
}
abs() { [ -n "$1" ] && printf '%s/%s' "$(cd "$(dirname "$1")" && pwd)" "$(basename "$1")"; }
[ -n "${DEB_PKG}" ] || DEB_PKG="$(discover_pkg 'yams-*.deb')"
[ -n "${RPM_PKG}" ] || RPM_PKG="$(discover_pkg 'yams-*.rpm')"
[ -n "${ARCH_PKG}" ] || ARCH_PKG="$(discover_pkg "yams-*-${ARCH_PKG_ARCH:-x86_64}.pkg.tar.zst")"
RUN_TAG="$$"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/yams-pkg-validate.XXXXXX")"
CONTAINERS=()
cleanup() {
if [ "${KEEP}" -eq 0 ]; then
for c in ${CONTAINERS[@]+"${CONTAINERS[@]}"}; do docker rm -f "${c}" >/dev/null 2>&1 || true; done
else
log "kept containers: ${CONTAINERS[*]+${CONTAINERS[*]}}"
fi
rm -rf "${WORK}"
}
trap cleanup EXIT
lane_fmt() { case "$1" in debian | ubuntu) echo deb ;; fedora) echo rpm ;; arch) echo arch ;; esac; }
lane_pkg() { case "$1" in debian | ubuntu) echo "${DEB_PKG}" ;; fedora) echo "${RPM_PKG}" ;; arch) echo "${ARCH_PKG}" ;; esac; }
lane_base_image() {
case "$1" in
debian) echo "${DEBIAN_IMAGE:-debian:trixie-slim}" ;;
ubuntu) echo "${UBUNTU_IMAGE:-ubuntu:24.04}" ;;
fedora) echo "${FEDORA_IMAGE:-fedora:42}" ;;
arch) echo "${ARCH_BASE_IMAGE:-archlinux/archlinux:latest}" ;;
esac
}
lane_upgrade_from() { case "$1" in debian | ubuntu) echo "${UP_DEB}" ;; fedora) echo "${UP_RPM}" ;; arch) echo "${UP_ARCH}" ;; esac; }
# Docker platform for a lane (empty = native). Bash 3.2 compatible (macOS):
# no mapfile, and empty arrays are expanded with ${a[@]+"${a[@]}"}.
lane_platform() { if [ "$1" = arch ]; then printf '%s' "${ARCH_DOCKER_PLATFORM:-linux/amd64}"; fi; }
build_substrate() { # build_substrate <lane> -> image tag on stdout
local lane="$1" image="yams/validate-$1:local" dockerfile
local -a args=()
case "${lane}" in
debian) dockerfile=debian-lane.Dockerfile; args=(--build-arg "BASE_IMAGE=${DEBIAN_IMAGE:-debian:trixie-slim}") ;;
ubuntu) dockerfile=debian-lane.Dockerfile; args=(--build-arg "BASE_IMAGE=${UBUNTU_IMAGE:-ubuntu:24.04}") ;;
fedora) dockerfile=fedora-lane.Dockerfile; args=(--build-arg "BASE_IMAGE=${FEDORA_IMAGE:-fedora:42}") ;;
arch)
dockerfile=arch-lane.Dockerfile
if [ -n "${ARCH_BASE_IMAGE:-}" ]; then args=(--build-arg "ARCH_BASE_IMAGE=${ARCH_BASE_IMAGE}"); fi
;;
esac
local plat
plat="$(lane_platform "${lane}")"
docker build -q ${plat:+--platform="${plat}"} ${args[@]+"${args[@]}"} \
-f "${SUBSTRATE_DIR}/${dockerfile}" -t "${image}" "${SUBSTRATE_DIR}" >/dev/null
printf '%s' "${image}"
}
boot() { # boot <lane> <image> <name>
local lane="$1" image="$2" name="$3"
local plat
plat="$(lane_platform "${lane}")"
docker rm -f "${name}" >/dev/null 2>&1 || true
docker run -d --name "${name}" ${plat:+--platform="${plat}"} --privileged --cgroupns=private \
--tmpfs /run --tmpfs /run/lock --tmpfs /tmp "${image}" >/dev/null
CONTAINERS+=("${name}")
wait_boot "${name}"
}
wait_boot() {
local name="$1" state=""
# Up to 3 minutes: a reboot on a loaded host can sit in 'starting' for a while.
for _ in $(seq 1 360); do
state="$(docker exec "${name}" systemctl is-system-running 2>/dev/null || true)"
case "${state}" in running | degraded) return 0 ;; esac
sleep 0.5
done
err "${name}: systemd did not boot (state='${state}')"
docker exec "${name}" systemctl list-jobs --no-pager 2>&1 | head -n 20 >&2 || true
docker logs "${name}" 2>&1 | tail -n 20 >&2 || true
return 1
}
reboot() {
docker restart "$1" >/dev/null
wait_boot "$1"
}
stage_files() { # stage_files <container> <file>...
local c="$1"
shift
docker exec "${c}" mkdir -p /var/tmp/yams-validate
docker cp "${CHECKS}" "${c}:/var/tmp/yams-validate/checks.sh"
for f in "$@"; do
if [ -n "${f}" ]; then docker cp "${f}" "${c}:/var/tmp/yams-validate/$(basename "${f}")"; fi
done
}
run_phase() { # run_phase <lane> <container> <phase> [pkg]
local lane="$1" c="$2" phase="$3" pkg="${4:-}" out="${WORK}/$1.results"
local arg="" rc=0
[ -n "${pkg}" ] && arg="/var/tmp/yams-validate/$(basename "${pkg}")"
log "${lane}: ${phase}"
docker exec "${c}" bash /var/tmp/yams-validate/checks.sh "${phase}" "$(lane_fmt "${lane}")" "${arg}" 2>&1 |
sed "s/^/${lane} /" | tee -a "${out}" | while IFS= read -r line; do
case "${line}" in
*" PASS "*) printf ' \033[32m%s\033[0m\n' "${line#* }" ;;
*" FAIL "*) printf ' \033[31m%s\033[0m\n' "${line#* }" ;;
*) printf ' %s\n' "${line#* }" ;;
esac
done || rc=$?
# Docker/transport errors may produce no child FAIL line for the summary.
if [ "${rc}" -ne 0 ]; then
note_fail "${lane}" "${phase}" "checks exited ${rc}"
fi
return "${rc}"
}
note_fail() { printf '%s FAIL %s %s\n' "$1" "$2" "$3" >>"${WORK}/$1.results"; err "$1: $2 $3"; }
validate_lane() {
local lane="$1" pkg upgrade_from image
pkg="$(abs "$(lane_pkg "${lane}")")" || true
upgrade_from="$(abs "$(lane_upgrade_from "${lane}")")" || true
: >"${WORK}/${lane}.results"
if [ -z "${pkg}" ] || [ ! -f "${pkg}" ]; then
note_fail "${lane}" package-missing "no $(lane_fmt "${lane}") package (pass it or build into ${BUILD_DIR})"
return
fi
log "${lane}: validating $(basename "${pkg}")"
if ! image="$(build_substrate "${lane}")"; then
note_fail "${lane}" substrate "failed to build substrate image"
return
fi
local c="yams-validate-${lane}-${RUN_TAG}"
if ! boot "${lane}" "${image}" "${c}"; then
note_fail "${lane}" boot "systemd did not boot"
return
fi
stage_files "${c}" "${pkg}"
if ! run_phase "${lane}" "${c}" install "${pkg}"; then return; fi
run_phase "${lane}" "${c}" layout
run_phase "${lane}" "${c}" service
run_phase "${lane}" "${c}" user
run_phase "${lane}" "${c}" onnx
log "${lane}: rebooting container"
if reboot "${c}"; then
run_phase "${lane}" "${c}" persist
else
note_fail "${lane}" reboot "container did not boot again"
fi
run_phase "${lane}" "${c}" remove
docker rm -f "${c}" >/dev/null 2>&1 || true
# Bare base image, no systemd, no lane tooling: only declared dependencies.
local m="yams-validate-${lane}-minimal-${RUN_TAG}" base plat
base="$(lane_base_image "${lane}")"
plat="$(lane_platform "${lane}")"
log "${lane}: minimal install on ${base}"
docker rm -f "${m}" >/dev/null 2>&1 || true
if docker run -d --name "${m}" ${plat:+--platform="${plat}"} "${base}" sleep infinity >/dev/null; then
CONTAINERS+=("${m}")
if [ "${lane}" = arch ]; then
# The Arch image's pacman sandbox needs these tweaks in containers.
docker exec "${m}" sh -c "grep -q '^DisableSandbox' /etc/pacman.conf || sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf; sed -i 's/^[[:space:]]*DownloadUser/# DownloadUser/' /etc/pacman.conf; pacman-key --init >/dev/null 2>&1; pacman-key --populate >/dev/null 2>&1" || true
fi
stage_files "${m}" "${pkg}"
run_phase "${lane}" "${m}" minimal "${pkg}"
docker rm -f "${m}" >/dev/null 2>&1 || true
else
note_fail "${lane}" minimal-boot "could not start ${base}"
fi
if [ -n "${upgrade_from}" ]; then
if [ ! -f "${upgrade_from}" ]; then
note_fail "${lane}" upgrade-from "missing ${upgrade_from}"
return
fi
local u="yams-validate-${lane}-upgrade-${RUN_TAG}"
if ! boot "${lane}" "${image}" "${u}"; then
note_fail "${lane}" upgrade-boot "systemd did not boot"
return
fi
stage_files "${u}" "${upgrade_from}" "${pkg}"
log "${lane}: upgrade path $(basename "${upgrade_from}") -> $(basename "${pkg}")"
# A failed install or seed cannot certify an upgrade against the old package.
if ! run_phase "${lane}" "${u}" install "${upgrade_from}"; then return; fi
if ! run_phase "${lane}" "${u}" seed; then return; fi
if ! run_phase "${lane}" "${u}" install "${pkg}"; then return; fi
run_phase "${lane}" "${u}" upgraded
docker rm -f "${u}" >/dev/null 2>&1 || true
fi
}
for lane in "${LANES[@]}"; do
validate_lane "${lane}" || note_fail "${lane}" harness "lane aborted"
done
# Summary
OVERALL=0
SUMMARY="${WORK}/summary.txt"
{
printf '%-8s %5s %5s %s\n' LANE PASS FAIL FAILED-CHECKS
for lane in "${LANES[@]}"; do
f="${WORK}/${lane}.results"
p="$(grep -c " PASS " "${f}" 2>/dev/null || true)"
n="$(grep -c " FAIL " "${f}" 2>/dev/null || true)"
failed="$(grep " FAIL " "${f}" 2>/dev/null | awk '{print $3}' | paste -sd, - || true)"
printf '%-8s %5s %5s %s\n' "${lane}" "${p:-0}" "${n:-0}" "${failed}"
[ "${n:-0}" -eq 0 ] || OVERALL=1
done
} >"${SUMMARY}"
echo
cat "${SUMMARY}"
if [ -n "${REPORT}" ]; then
{
cat "${SUMMARY}"
echo
cat "${WORK}"/*.results
} >"${REPORT}"
fi
if [ "${OVERALL}" -eq 0 ]; then okmsg "all package validation lanes passed"; else err "package validation failed"; fi
exit "${OVERALL}"