#!/bin/bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" if [[ $# -lt 1 || $# -gt 2 ]]; then echo "Usage: $0 CANDIDATE.deb [PREVIOUS.deb]" >&2 exit 2 fi candidate="$(realpath "$1")" previous="" [[ -f "$candidate" ]] || { echo "Package not found: $candidate" >&2; exit 1; } if [[ $# -eq 2 ]]; then previous="$(realpath "$2")" [[ -f "$previous" ]] || { echo "Package not found: $previous" >&2; exit 1; } fi echo "Validating $(basename "$candidate") on Debian Trixie" docker_args=( run --rm -i -v "$candidate:/packages/candidate.deb:ro" ) if [[ -n "$previous" ]]; then docker_args+=( -v "$previous:/packages/previous.deb:ro" ) fi timeout --signal=TERM --kill-after=15s 720s \ docker "${docker_args[@]}" debian:trixie-slim \ bash -s -- "${previous:+/packages/previous.deb}" <<'CONTAINER' set -Eeuo pipefail validation_failure() { status=$? echo "Validation failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND}" >&2 if command -v lpstat >/dev/null 2>&1; then lpstat -t >&2 || true lpoptions -p Elmer -l >&2 || true fi [[ ! -f /tmp/provision-first.log ]] || cat /tmp/provision-first.log >&2 [[ ! -f /tmp/provision-second.log ]] || cat /tmp/provision-second.log >&2 exit "$status" } trap validation_failure ERR export DEBIAN_FRONTEND=noninteractive # Package validation runs in a container with no init system. Do not let # maintainer scripts try to start CUPS, Avahi or other irrelevant daemons; # services are inspected and CUPS is started explicitly when the test needs it. cat > /usr/sbin/policy-rc.d <<'POLICY' #!/bin/sh exit 101 POLICY chmod 0755 /usr/sbin/policy-rc.d run_logged() { label="$1" seconds="$2" shift 2 if ! timeout --signal=TERM --kill-after=10s "${seconds}s" "$@" \ >"/tmp/${label}.log" 2>&1; then cat "/tmp/${label}.log" >&2 echo "${label} exceeded ${seconds}s or failed" >&2 exit 1 fi } run_logged apt-update 120 apt-get update -qq previous="${1:-}" if [[ -n "$previous" ]]; then run_logged previous-install 360 apt-get install -y -qq "$previous" cat > /etc/ekklesia-classroom/device.conf <<'CONFIG' # Configuration carried forward from 0.1.36 UPDATE_URL=https://updates.riverofglorymn.org CHANNEL=canary DVD_ENABLED=false UI_SCALE=1.25 MQTT_ENABLED=true MQTT_HOST=broker.example.test MQTT_PORT=1884 MQTT_USERNAME=classroom MQTT_PASSWORD=preserve-me CUSTOM_SETTING=preserve-me-too CONFIG fi run_logged candidate-install 360 apt-get install -y -qq /packages/candidate.deb command -v nmtui >/dev/null command -v lpadmin >/dev/null command -v lpinfo >/dev/null systemctl is-enabled NetworkManager.service >/dev/null systemctl is-enabled cups.service >/dev/null ! systemctl is-enabled NetworkManager-wait-online.service >/dev/null 2>&1 # The audio policy reaches a session, and is ordered after the server it # actually talks to. Ordering it only after pipewire and wireplumber is # what let it race PipeWire's PulseAudio server at boot, fail its one # startup attempt and then sit idle — the rooms were silent on 0.2.2 for # exactly this reason, so the ordering is asserted rather than assumed. audio_unit=/etc/systemd/user/ekklesia-classroom-audio.service [[ -f "$audio_unit" ]] grep -q 'pipewire-pulse' "$audio_unit" [[ -L /etc/systemd/user/plasma-workspace.target.wants/ekklesia-classroom-audio.service ]] command -v classroom-audio >/dev/null command -v pactl >/dev/null # The policy module and the daemon agree on what they import, on a machine # rather than in a checkout. PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \ 'import classroom_audio; classroom_audio.settled([], "auto", None)' grep -q '^managed=false$' /etc/NetworkManager/NetworkManager.conf # A normal Docker container has Ethernet and no Wi-Fi adapter. Reaching this # point proves package configuration did not require Wi-Fi hardware. ! find /sys/class/net -mindepth 2 -maxdepth 2 -type d -name wireless | grep -q . /usr/sbin/cupsd sleep 1 driver='foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd' lpinfo -m | grep -q "^${driver} " # Simulate the queue an older release may have left behind, then run the real # boot provisioning path. Chrome is stubbed as already installed and DVD is # disabled so the test remains entirely local and deterministic. lpadmin -p Classroom_Color -v socket://192.0.2.10:9100 -m "$driver" install -m755 /bin/true /usr/local/bin/google-chrome timeout --signal=TERM --kill-after=5s 45s classroom-provision \ >/tmp/provision-first.log ! lpstat -p Classroom_Color >/dev/null 2>&1 lpstat -p Elmer -d -v | grep -q 'socket://10.10.0.155:9100' lpstat -l -p Elmer | grep -q 'Description: Elmer' lpoptions -p Elmer -l | grep -Eq '^PrintoutMode/.*:.*\*Normal([[:space:]]|$)' lpoptions -p Elmer -l | grep -Eq '^ColorModel/.*:.*\*Color([[:space:]]|$)' [[ "$(lpstat -e)" == "Elmer" ]] [[ "$(systemctl is-enabled avahi-daemon.service 2>/dev/null || true)" == "masked" ]] [[ "$(systemctl is-enabled avahi-daemon.socket 2>/dev/null || true)" == "masked" ]] timeout --signal=TERM --kill-after=5s 45s classroom-provision \ >/tmp/provision-second.log grep -q 'Elmer is already correctly configured' /tmp/provision-second.log if [[ -n "$previous" ]]; then # The room's own values, untouched by the upgrade. grep -q '^DVD_ENABLED=false$' /etc/ekklesia-classroom/device.conf grep -q '^UI_SCALE=1.25$' /etc/ekklesia-classroom/device.conf grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf # The channel a room was put on deliberately survives the upgrade. # canary is where the next development release goes, and the Plasma # classroom is the stable line now, so nothing here has to move. grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf # And a room that was carried through the experiment on its own # channel comes home, because that channel is the obsolete one now. sed -i 's/^CHANNEL=.*/CHANNEL=classroom-v2/' /etc/ekklesia-classroom/device.conf classroom-migrate >/tmp/migrate-from-v2.log 2>&1 grep -q '^CHANNEL=stable$' /etc/ekklesia-classroom/device.conf grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf sed -i 's/^CHANNEL=.*/CHANNEL=canary/' /etc/ekklesia-classroom/device.conf # Every setting this release has that the room predates, appended # exactly once. classroom-migrate hands device.conf to the shared # helper after its own work for this, so that a machine converges # whether it got here through this postinst or through somebody # running the command by hand. for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do [[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]] done run_logged candidate-reinstall 360 \ apt-get install --reinstall -y -qq /packages/candidate.deb # An install that changes nothing must change nothing. for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do [[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]] done grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf # And by hand, on a machine that is already converged: the command a # technician runs must be safe to run twice. classroom-migrate >/tmp/migrate-by-hand.log 2>&1 for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do [[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]] done grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf ! grep -q 'preserve-me' /tmp/migrate-by-hand.log fi echo "Trixie package, upgrade, bounded install, networking, Elmer printing and reinstall checks passed." CONTAINER