0.2.6.1 is a maintenance release of 0.2.6. The build refused four parts, update_canary.sh's automatic bump could not add one to "6.1", and neither publish script would have noticed a version being published twice — which is what happened to 0.2.6 this morning: it was rebuilt with the Presentations hardening and uploaded over the first 0.2.6, and a room that had installed the first can never be offered the second. One release library now holds the version pattern (three or four parts, no leading zeros, no .0 maintenance), orders by dpkg --compare-versions, refuses a version that has ever been published or is not newer than all of them, uploads with --ignore-existing and checks the server's checksum, writes a manifest only for a package the server holds, and reads the channel back over HTTPS afterwards. update_canary.sh takes the version explicitly; update_stable.sh refuses to go backwards. The classroom updater refuses a manifest whose version, package name or checksum is malformed, and refuses a download whose own control file is not the version the manifest promised, so a room can never loop on reinstalling and rebooting. tests/test-release-versions.sh: 87 checks against a stand-in server and real dpkg. The Trixie gate now also rehearses a stable room: 0.2.2 to an interim 0.2.6.1 upgraded with settings, channel and Presentations=false intact, and the installed updater answering correctly. Co-Authored-By: Claude Opus 5.5 <[email protected]>
550 lines
20 KiB
Bash
Executable File
550 lines
20 KiB
Bash
Executable File
#!/bin/bash
|
|
# Three- and four-part versions, through everything that builds, publishes
|
|
# and installs a classroom package.
|
|
#
|
|
# Nothing here reaches the real update server. The server is a directory,
|
|
# ssh and rsync are stand-ins that act on it, and it is served over HTTP
|
|
# on the loopback interface so that the real updater, the real channel
|
|
# check and the real dpkg are the ones being asked.
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
WORK="$(mktemp -d)"
|
|
SERVER_PID=""
|
|
|
|
cleanup() {
|
|
[[ -z "$SERVER_PID" ]] || kill "$SERVER_PID" 2>/dev/null
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
passed=0
|
|
failed=0
|
|
|
|
check() {
|
|
local label="$1"
|
|
shift
|
|
|
|
if "$@" >/dev/null 2>&1; then
|
|
echo " ok $label"
|
|
passed=$((passed + 1))
|
|
else
|
|
echo " FAIL $label"
|
|
failed=$((failed + 1))
|
|
fi
|
|
}
|
|
|
|
refuses() {
|
|
! "$@"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# A package that is nothing but its version, the way dpkg will read it.
|
|
fake_deb() {
|
|
local version="$1" out="$2" stage="$WORK/stage-$version"
|
|
|
|
mkdir -p "$stage/DEBIAN"
|
|
cat > "$stage/DEBIAN/control" <<EOF
|
|
Package: ${3:-ekklesia-classroom}
|
|
Version: $version
|
|
Architecture: amd64
|
|
Maintainer: test
|
|
Description: test package
|
|
EOF
|
|
dpkg-deb --root-owner-group --build "$stage" "$out" >/dev/null
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The update server, as a directory.
|
|
SERVER="$WORK/server"
|
|
LOCAL="$WORK/local"
|
|
mkdir -p "$SERVER/packages" "$SERVER/stable" "$SERVER/canary" \
|
|
"$LOCAL/packages" "$LOCAL/canary" "$LOCAL/stable" "$WORK/bin"
|
|
|
|
for version in 0.1.45~hypr1 0.1.45 0.2.2 0.2.5 0.2.6; do
|
|
fake_deb "$version" "$SERVER/packages/ekklesia-classroom_${version}_amd64.deb"
|
|
cp "$SERVER/packages/ekklesia-classroom_${version}_amd64.deb" "$LOCAL/packages/"
|
|
done
|
|
# Not a classroom package, and on the real server all the same.
|
|
echo v2 > "$SERVER/packages/ekklesia-classroom-v2.deb"
|
|
|
|
manifest() {
|
|
local version="$1" file="${2:-ekklesia-classroom_$1_amd64.deb}" hash
|
|
hash="$(sha256sum "$SERVER/packages/$file" | awk '{print $1}')"
|
|
printf '{\n "version": "%s",\n "package": "/packages/%s",\n "sha256": "%s",\n "requires_reboot": true\n}\n' \
|
|
"$version" "$file" "$hash"
|
|
}
|
|
|
|
manifest 0.2.2 > "$SERVER/stable/manifest.json"
|
|
manifest 0.2.6 > "$SERVER/canary/manifest.json"
|
|
ln -s ../packages/ekklesia-classroom_0.2.2_amd64.deb "$SERVER/stable/latest.deb"
|
|
ln -s ../packages/ekklesia-classroom_0.2.6_amd64.deb "$SERVER/canary/latest.deb"
|
|
cp "$SERVER/stable/manifest.json" "$LOCAL/stable/"
|
|
cp "$SERVER/canary/manifest.json" "$LOCAL/canary/"
|
|
|
|
# ssh HOST COMMAND: run COMMAND here. The release scripts only ever talk
|
|
# to one host, and it is this directory.
|
|
cat > "$WORK/bin/ssh" <<'EOF'
|
|
#!/bin/bash
|
|
shift
|
|
exec bash -c "$*"
|
|
EOF
|
|
|
|
# rsync [-av] [--ignore-existing] -e ssh SOURCE HOST:DEST
|
|
cat > "$WORK/bin/rsync" <<'EOF'
|
|
#!/bin/bash
|
|
keep=0
|
|
args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--ignore-existing) keep=1 ;;
|
|
-e) shift ;;
|
|
-*) ;;
|
|
*) args+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
source="${args[0]}"
|
|
dest="${args[1]#*:}"
|
|
[[ -d "$dest" ]] && dest="$dest/$(basename "$source")"
|
|
if [[ "$keep" == 1 && -e "$dest" ]]; then
|
|
exit 0
|
|
fi
|
|
cp "$source" "$dest"
|
|
EOF
|
|
chmod +x "$WORK/bin/ssh" "$WORK/bin/rsync"
|
|
|
|
PORT="$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')"
|
|
python3 -m http.server --bind 127.0.0.1 --directory "$SERVER" "$PORT" \
|
|
>/dev/null 2>&1 &
|
|
SERVER_PID=$!
|
|
|
|
for _ in $(seq 1 50); do
|
|
curl -fsS "http://127.0.0.1:$PORT/stable/manifest.json" >/dev/null 2>&1 && break
|
|
sleep 0.1
|
|
done
|
|
|
|
echo "0. the library, sourced the way the release scripts source it"
|
|
# With nothing in the environment and set -u in force. Every test below
|
|
# exports its own RELEASE_* values, and a default that only worked when
|
|
# somebody had already set it once stopped the real build dead.
|
|
check "it loads under set -u with nothing set" \
|
|
env -i PATH="$PATH" bash -c "set -euo pipefail; ROOT=/checkout; source '$ROOT/tools/release-lib.sh'
|
|
[[ \$RELEASE_HOST == classroom-control ]]
|
|
[[ \$RELEASE_ROOT == /srv/ekklesia/classroom-updates ]]
|
|
[[ \$RELEASE_URL == https://updates.riverofglorymn.org ]]
|
|
[[ \$RELEASE_LOCAL == /checkout/repo ]]"
|
|
check "and build-release.sh gets as far as checking the version" \
|
|
bash -c "'$ROOT/tools/build-release.sh' 0.2.06 2>&1 | grep -q 'Invalid version: 0.2.06'"
|
|
echo
|
|
|
|
export PATH="$WORK/bin:$PATH"
|
|
export RELEASE_HOST=test-server RELEASE_ROOT="$SERVER" \
|
|
RELEASE_URL="http://127.0.0.1:$PORT" RELEASE_LOCAL="$LOCAL"
|
|
|
|
# shellcheck source=../tools/release-lib.sh
|
|
source "$ROOT/tools/release-lib.sh"
|
|
|
|
GOOD=(0.2.2 0.2.6 0.2.6.1 0.2.6.2 0.2.7 0.3.0 0.2.10 0.2.6.10 1.0.0 10.20.30.40)
|
|
BAD=("" 0.2 0.2.6.1.1 0.2.06 00.2.6 0.2.6.0 0.2.6.01 v0.2.6 0.2.6-1 0.2.6~rc1
|
|
"0.2.6 " " 0.2.6" 0.2.6. .0.2.6 0..2 0.2.a "0.2.6;reboot" "0.2.6
|
|
0.2.7" 0.2.6+1 -0.2.6)
|
|
|
|
echo "1. what a version may look like"
|
|
for version in "${GOOD[@]}"; do
|
|
check "the release scripts accept $version" release_version_valid "$version"
|
|
done
|
|
for version in "${BAD[@]}"; do
|
|
check "the release scripts refuse '${version//$'\n'/\\n}'" refuses release_version_valid "$version"
|
|
done
|
|
|
|
# The updater decides with its own copy of the pattern, because it runs
|
|
# on a classroom that has no checkout. The two copies are held to the
|
|
# same list.
|
|
python3 - "$ROOT/scripts/ekklesia-classroom-update" "${GOOD[@]}" -- "${BAD[@]}" <<'PY'
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import sys
|
|
|
|
loader = importlib.machinery.SourceFileLoader("updater", sys.argv[1])
|
|
spec = importlib.util.spec_from_loader("updater", loader)
|
|
updater = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(updater)
|
|
|
|
rest = sys.argv[2:]
|
|
split = rest.index("--")
|
|
good, bad = rest[:split], rest[split + 1:]
|
|
wrong = [v for v in good if not updater.valid_version(v)]
|
|
wrong += [v for v in bad if updater.valid_version(v)]
|
|
wrong += [v for v in (None, 26, 0.26, ["0.2.6"]) if updater.valid_version(v)]
|
|
|
|
for version in wrong:
|
|
print(f" FAIL the updater disagrees about {version!r}")
|
|
|
|
sys.exit(1 if wrong else 0)
|
|
PY
|
|
check "the updater agrees about every one of them, and refuses non-strings" test $? -eq 0
|
|
|
|
echo
|
|
echo "2. which version is newer, as dpkg decides it"
|
|
ordered=(0.1.45~hypr1 0.1.45 0.2.2 0.2.5 0.2.6 0.2.6.1 0.2.6.2 0.2.6.10 0.2.7 0.2.10 0.3.0)
|
|
for ((i = 1; i < ${#ordered[@]}; i++)); do
|
|
check "${ordered[i-1]} < ${ordered[i]}" \
|
|
release_version_gt "${ordered[i]}" "${ordered[i-1]}"
|
|
check "and not the other way round" \
|
|
refuses release_version_gt "${ordered[i-1]}" "${ordered[i]}"
|
|
done
|
|
check "0.2.6.1 is not newer than itself" refuses release_version_gt 0.2.6.1 0.2.6.1
|
|
check "the highest of everything published is 0.2.6" \
|
|
test "$(release_published_versions | release_highest)" = 0.2.6
|
|
|
|
echo
|
|
echo "3. a version may be published once, and only if it is newer"
|
|
check "0.2.6.1 may be published" release_check_new_version 0.2.6.1
|
|
check "0.2.7 may be published" release_check_new_version 0.2.7
|
|
check "0.2.6 may not: it is already on the server" refuses release_check_new_version 0.2.6
|
|
check "0.2.5.1 may not: it is older than 0.2.6" refuses release_check_new_version 0.2.5.1
|
|
check "0.2.2 may not" refuses release_check_new_version 0.2.2
|
|
check "0.2.06 may not: dpkg would call it 0.2.6" refuses release_check_new_version 0.2.06
|
|
check "0.2.6.0 may not" refuses release_check_new_version 0.2.6.0
|
|
|
|
# Published only in the local copy — an upload that failed after the copy
|
|
# was made. The number is spent all the same.
|
|
fake_deb 0.2.8 "$LOCAL/packages/ekklesia-classroom_0.2.8_amd64.deb"
|
|
check "a version already copied into repo/ is spent too" refuses release_check_new_version 0.2.8
|
|
rm "$LOCAL/packages/ekklesia-classroom_0.2.8_amd64.deb"
|
|
|
|
echo
|
|
echo "4. uploading never replaces a published package"
|
|
fake_deb 0.2.6.1 "$WORK/ekklesia-classroom_0.2.6.1_amd64.deb"
|
|
check "0.2.6.1 uploads and arrives intact" \
|
|
release_upload_package "$WORK/ekklesia-classroom_0.2.6.1_amd64.deb"
|
|
check "and the server copy is the built one" \
|
|
cmp "$WORK/ekklesia-classroom_0.2.6.1_amd64.deb" \
|
|
"$SERVER/packages/ekklesia-classroom_0.2.6.1_amd64.deb"
|
|
|
|
before="$(sha256sum "$SERVER/packages/ekklesia-classroom_0.2.6_amd64.deb")"
|
|
mkdir -p "$WORK/rebuilt"
|
|
fake_deb 0.2.6 "$WORK/rebuilt/ekklesia-classroom_0.2.6_amd64.deb" ekklesia-classroom-rebuilt
|
|
check "a different 0.2.6 is refused" \
|
|
refuses release_upload_package "$WORK/rebuilt/ekklesia-classroom_0.2.6_amd64.deb"
|
|
check "and the published 0.2.6 is untouched" \
|
|
test "$(sha256sum "$SERVER/packages/ekklesia-classroom_0.2.6_amd64.deb")" = "$before"
|
|
|
|
echo
|
|
echo "5. a channel points at exactly what was uploaded"
|
|
hash_0261="$(sha256sum "$WORK/ekklesia-classroom_0.2.6.1_amd64.deb" | awk '{print $1}')"
|
|
stable_before="$(cat "$SERVER/stable/manifest.json")"
|
|
|
|
check "a manifest promising a different checksum is refused" \
|
|
refuses release_publish_manifest canary 0.2.6.1 "$(printf '0%.0s' {1..64})"
|
|
check "and canary still says 0.2.6" test "$(release_channel_version canary)" = 0.2.6
|
|
|
|
check "canary is moved to 0.2.6.1" release_publish_manifest canary 0.2.6.1 "$hash_0261"
|
|
check "its manifest says so" test "$(release_channel_version canary)" = 0.2.6.1
|
|
check "latest.deb follows it" \
|
|
test "$(readlink "$SERVER/canary/latest.deb")" = ../packages/ekklesia-classroom_0.2.6.1_amd64.deb
|
|
check "what a classroom downloads agrees, over HTTP" release_verify_channel canary 0.2.6.1
|
|
check "stable's manifest was not touched" \
|
|
test "$(cat "$SERVER/stable/manifest.json")" = "$stable_before"
|
|
check "and stable still verifies as 0.2.2" release_verify_channel stable 0.2.2
|
|
check "the check notices a channel saying the wrong version" \
|
|
refuses release_verify_channel stable 0.2.6.1
|
|
check "0.2.6.1 is now spent" refuses release_check_new_version 0.2.6.1
|
|
check "0.2.6.2 is next in line" release_check_new_version 0.2.6.2
|
|
|
|
# A package on the server that says it is a different version from its
|
|
# name. The channel check has to read the package, not just its name.
|
|
fake_deb 0.2.6 "$SERVER/packages/ekklesia-classroom_0.2.9_amd64.deb"
|
|
manifest 0.2.9 > "$SERVER/canary/manifest.json"
|
|
ln -sfn ../packages/ekklesia-classroom_0.2.9_amd64.deb "$SERVER/canary/latest.deb"
|
|
check "a package that says it is another version fails the channel check" \
|
|
refuses release_verify_channel canary 0.2.9
|
|
rm "$SERVER/packages/ekklesia-classroom_0.2.9_amd64.deb"
|
|
manifest 0.2.6.1 > "$SERVER/canary/manifest.json"
|
|
ln -sfn ../packages/ekklesia-classroom_0.2.6.1_amd64.deb "$SERVER/canary/latest.deb"
|
|
|
|
echo
|
|
echo "6. update_stable.sh"
|
|
cp "$WORK/ekklesia-classroom_0.2.6.1_amd64.deb" "$LOCAL/packages/"
|
|
check "refuses a version that is not newer than stable" \
|
|
refuses "$ROOT/update_stable.sh" 0.2.2
|
|
check "refuses a malformed version" refuses "$ROOT/update_stable.sh" 0.2.06
|
|
fake_deb 0.2.9 "$LOCAL/packages/ekklesia-classroom_0.2.9_amd64.deb"
|
|
check "refuses a package that was built but never uploaded" \
|
|
refuses "$ROOT/update_stable.sh" 0.2.9
|
|
rm "$LOCAL/packages/ekklesia-classroom_0.2.9_amd64.deb"
|
|
check "stable is still 0.2.2 after all of that" \
|
|
test "$(cat "$SERVER/stable/manifest.json")" = "$stable_before"
|
|
|
|
echo
|
|
echo "7. update_canary.sh"
|
|
check "without a version it builds nothing and names both choices" \
|
|
bash -c "'$ROOT/update_canary.sh' 2>&1 | grep -q '0.2.6.2 .*maintenance' \
|
|
&& '$ROOT/update_canary.sh' 2>&1 | grep -q '0.2.7 .*feature' \
|
|
&& ! '$ROOT/update_canary.sh'"
|
|
check "a published version is refused before anything is built" \
|
|
bash -c "! '$ROOT/update_canary.sh' 0.2.6 2>&1 | grep -q 'Validating sources'"
|
|
check "a malformed version is refused" refuses "$ROOT/update_canary.sh" 0.2.6.1.1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "8. a classroom's updater, against that server"
|
|
|
|
# The package database and apt, as far as the updater can tell. The
|
|
# stand-in apt installs by reading the version out of the file it is
|
|
# given, which is what the real one does.
|
|
cat > "$WORK/bin/dpkg-query" <<EOF
|
|
#!/bin/bash
|
|
[[ -s "$WORK/installed" ]] || exit 1
|
|
cat "$WORK/installed"
|
|
EOF
|
|
cat > "$WORK/bin/apt-get" <<EOF
|
|
#!/bin/bash
|
|
echo "\$*" >> "$WORK/apt.log"
|
|
dpkg-deb --field "\${@: -1}" Version > "$WORK/installed"
|
|
EOF
|
|
cat > "$WORK/bin/systemctl" <<EOF
|
|
#!/bin/bash
|
|
echo "\$*" >> "$WORK/systemctl.log"
|
|
EOF
|
|
chmod +x "$WORK/bin/dpkg-query" "$WORK/bin/apt-get" "$WORK/bin/systemctl"
|
|
|
|
UPDATE_URL="http://127.0.0.1:$PORT" WORK="$WORK" SERVER="$SERVER" \
|
|
python3 - "$ROOT/scripts/ekklesia-classroom-update" <<'PY'
|
|
import contextlib
|
|
import hashlib
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
work = Path(os.environ["WORK"])
|
|
server = Path(os.environ["SERVER"])
|
|
|
|
loader = importlib.machinery.SourceFileLoader("updater", sys.argv[1])
|
|
spec = importlib.util.spec_from_loader("updater", loader)
|
|
updater = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(updater)
|
|
|
|
updater.LOCKFILE = str(work / "update.lock")
|
|
updater.CACHE_DIR = str(work / "cache")
|
|
|
|
passed = failed = 0
|
|
|
|
|
|
def check(label, condition, detail=""):
|
|
global passed, failed
|
|
if condition:
|
|
print(f" ok {label}")
|
|
passed += 1
|
|
else:
|
|
print(f" FAIL {label}")
|
|
if detail:
|
|
print(f" {detail}")
|
|
failed += 1
|
|
|
|
|
|
def device(channel, installed):
|
|
config = work / f"device-{channel}.conf"
|
|
config.write_text(
|
|
"# a classroom\n"
|
|
f"UPDATE_URL={os.environ['UPDATE_URL']}\n"
|
|
f"CHANNEL={channel}\n"
|
|
"PRESENTATIONS_ENABLED=false\n"
|
|
"UI_SCALE=1.25\n"
|
|
)
|
|
updater.CONFIG = str(config)
|
|
(work / "installed").write_text(installed)
|
|
for log in ("apt.log", "systemctl.log"):
|
|
(work / log).unlink(missing_ok=True)
|
|
return config
|
|
|
|
|
|
def run(*args):
|
|
sys.argv = ["ekklesia-classroom-update", *args]
|
|
out = io.StringIO()
|
|
with contextlib.redirect_stdout(out):
|
|
status = updater.main()
|
|
return status, out.getvalue()
|
|
|
|
|
|
def installs():
|
|
log = work / "apt.log"
|
|
return log.read_text().splitlines() if log.exists() else []
|
|
|
|
|
|
def installed():
|
|
return (work / "installed").read_text().strip()
|
|
|
|
|
|
def deb(version, name=None, package="ekklesia-classroom"):
|
|
stage = work / f"stage-{version}-{package}"
|
|
(stage / "DEBIAN").mkdir(parents=True, exist_ok=True)
|
|
(stage / "DEBIAN" / "control").write_text(
|
|
f"Package: {package}\nVersion: {version}\nArchitecture: amd64\n"
|
|
"Maintainer: test\nDescription: test\n"
|
|
)
|
|
out = server / "packages" / (name or f"ekklesia-classroom_{version}_amd64.deb")
|
|
out.unlink(missing_ok=True)
|
|
subprocess.run(["dpkg-deb", "--root-owner-group", "--build", stage, out],
|
|
check=True, capture_output=True)
|
|
return out
|
|
|
|
|
|
def publish(channel, version, file=None, sha=None, raw=None):
|
|
path = server / channel / "manifest.json"
|
|
if raw is not None:
|
|
path.write_text(raw)
|
|
return
|
|
file = file or f"ekklesia-classroom_{version}_amd64.deb"
|
|
digest = sha or hashlib.sha256((server / "packages" / file).read_bytes()).hexdigest()
|
|
path.write_text(json.dumps({
|
|
"version": version, "package": f"/packages/{file}",
|
|
"sha256": digest, "requires_reboot": True,
|
|
}))
|
|
|
|
|
|
publish("stable", "0.2.2")
|
|
publish("canary", "0.2.6.1")
|
|
|
|
config = device("canary", "0.2.6")
|
|
before = config.read_text()
|
|
status, out = run("--check")
|
|
check("a canary device on 0.2.6 sees 0.2.6.1 as an update",
|
|
status == 0 and "Update available: 0.2.6 -> 0.2.6.1" in out, out)
|
|
check("and --check installs nothing", installs() == [])
|
|
|
|
status, out = run("--no-reboot")
|
|
check("it installs 0.2.6.1", status == 0 and installed() == "0.2.6.1", out)
|
|
check("once, from the versioned file",
|
|
len(installs()) == 1 and installs()[0].endswith("ekklesia-classroom_0.2.6.1_amd64.deb"),
|
|
installs())
|
|
check("--no-reboot is respected", not (work / "systemctl.log").exists())
|
|
check("its device.conf is exactly as it was", config.read_text() == before)
|
|
|
|
for attempt in range(3):
|
|
run("--no-reboot")
|
|
check("a device on 0.2.6.1 does not install 0.2.6.1 again",
|
|
len(installs()) == 1, installs())
|
|
status, out = run()
|
|
check("nor reboot for it", "Already up to date" in out
|
|
and not (work / "systemctl.log").exists(), out)
|
|
|
|
deb("0.2.7")
|
|
publish("canary", "0.2.7")
|
|
status, out = run()
|
|
check("a device on 0.2.6.1 recognises 0.2.7 as newer and installs it",
|
|
status == 0 and installed() == "0.2.7", out)
|
|
check("and reboots, because the manifest asks it to",
|
|
(work / "systemctl.log").read_text().strip() == "reboot")
|
|
publish("canary", "0.2.6.1")
|
|
|
|
device("canary", "0.2.7")
|
|
status, out = run()
|
|
check("a device on 0.2.7 is not moved back to 0.2.6.1",
|
|
"Already up to date" in out and installs() == [], out)
|
|
|
|
# Three parts, exactly as before.
|
|
publish("canary", "0.2.6")
|
|
device("canary", "0.2.5")
|
|
status, out = run("--no-reboot")
|
|
check("a device on 0.2.5 still updates to 0.2.6", installed() == "0.2.6", out)
|
|
publish("canary", "0.2.6.1")
|
|
|
|
# The channels.
|
|
config = device("stable", "0.2.2")
|
|
status, out = run("--no-reboot")
|
|
check("a stable device on 0.2.2 reads the stable manifest",
|
|
f"{os.environ['UPDATE_URL']}/stable/manifest.json" in out, out)
|
|
check("and does not install the canary release",
|
|
installs() == [] and installed() == "0.2.2" and "Already up to date" in out, out)
|
|
|
|
deb("0.2.6.2")
|
|
publish("stable", "0.2.6.2")
|
|
status, out = run("--no-reboot")
|
|
check("a stable device goes from 0.2.2 straight to a four-part release when stable has one",
|
|
installed() == "0.2.6.2", out)
|
|
publish("stable", "0.2.2")
|
|
(server / "packages" / "ekklesia-classroom_0.2.6.2_amd64.deb").unlink()
|
|
|
|
# A room with no package installed at all reports 0.0.0 and takes anything.
|
|
device("canary", "")
|
|
status, out = run("--check")
|
|
check("a machine with nothing installed is offered 0.2.6.1",
|
|
"0.0.0 -> 0.2.6.1" in out, out)
|
|
|
|
# Malformed manifests: nothing downloaded, nothing installed, and a
|
|
# failure the timer's journal will show.
|
|
sha = hashlib.sha256((server / "packages" / "ekklesia-classroom_0.2.6.1_amd64.deb").read_bytes()).hexdigest()
|
|
bad = {
|
|
"a version with a suffix": {"version": "0.2.6.1-1"},
|
|
"a version with five parts": {"version": "0.2.6.1.1"},
|
|
"a version with a leading zero": {"version": "0.2.06"},
|
|
"a version that is a number": {"version": 0.3},
|
|
"an empty version": {"version": ""},
|
|
"a version with a shell in it": {"version": "0.2.7;reboot"},
|
|
"no version at all": {"version": None},
|
|
"a package for a different version": {"version": "0.2.7",
|
|
"package": "/packages/ekklesia-classroom_0.2.6.1_amd64.deb"},
|
|
"a package outside /packages": {"version": "0.2.7",
|
|
"package": "/../ekklesia-classroom_0.2.7_amd64.deb"},
|
|
"a checksum that is not one": {"version": "0.2.7", "sha256": "abc"},
|
|
}
|
|
for label, fields in bad.items():
|
|
manifest = {"version": "0.2.7",
|
|
"package": "/packages/ekklesia-classroom_0.2.7_amd64.deb",
|
|
"sha256": sha, "requires_reboot": True}
|
|
manifest.update(fields)
|
|
if manifest["version"] is None:
|
|
del manifest["version"]
|
|
publish("canary", None, raw=json.dumps(manifest))
|
|
device("canary", "0.2.6")
|
|
status, out = run("--no-reboot")
|
|
check(f"refused: {label}",
|
|
status == 1 and installs() == [] and installed() == "0.2.6", out)
|
|
|
|
for label, raw in (("a manifest that is not JSON", "<html>502</html>"),
|
|
("a manifest that is a list", "[]")):
|
|
publish("canary", None, raw=raw)
|
|
device("canary", "0.2.6")
|
|
status, out = run("--no-reboot")
|
|
check(f"refused: {label}", status == 1 and installs() == [], out)
|
|
|
|
# The file arrives, matches its checksum, and is the wrong package.
|
|
shutil.rmtree(updater.CACHE_DIR, ignore_errors=True)
|
|
deb("0.2.6", name="ekklesia-classroom_0.2.7_amd64.deb")
|
|
publish("canary", "0.2.7")
|
|
device("canary", "0.2.6")
|
|
status, out = run("--no-reboot")
|
|
check("a 0.2.7 file that installs as 0.2.6 is refused",
|
|
status == 1 and installs() == [] and "not ekklesia-classroom 0.2.7" in out, out)
|
|
check("and not left in the cache",
|
|
not list(Path(updater.CACHE_DIR).glob("*0.2.7*")))
|
|
|
|
deb("0.2.7", package="something-else", name="ekklesia-classroom_0.2.7_amd64.deb")
|
|
publish("canary", "0.2.7")
|
|
status, out = run("--no-reboot")
|
|
check("so is a file that is some other package", status == 1 and installs() == [], out)
|
|
|
|
deb("0.2.7")
|
|
publish("canary", "0.2.7", sha="0" * 64)
|
|
status, out = run("--no-reboot")
|
|
check("and a file whose checksum is wrong",
|
|
status == 1 and installs() == [] and "verification FAILED" in out, out)
|
|
|
|
publish("canary", "0.2.6.1")
|
|
print(f"\n updater: {passed} passed, {failed} failed")
|
|
sys.exit(1 if failed else 0)
|
|
PY
|
|
check "every updater case above passed" test $? -eq 0
|
|
|
|
echo
|
|
echo "$passed passed, $failed failed"
|
|
[[ "$failed" -eq 0 ]]
|