feat: four-part versions, and a published version is never used again

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]>
This commit is contained in:
ekklesia
2026-09-23 18:19:37 -05:00
co-authored by Claude Opus 5.5
parent 6f2d169355
commit 690ef95aff
9 changed files with 1260 additions and 147 deletions
+41 -55
View File
@@ -4,76 +4,62 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
source "$ROOT/tools/release-lib.sh"
# Stable never gets a package of its own. It is pointed at one canary
# already published, so the file every classroom downloads is byte for
# byte the one the canary machine ran.
if [[ $# -eq 1 ]]; then
version="$1"
package_name="ekklesia-classroom_${version}_amd64.deb"
package_path="$ROOT/repo/packages/$package_name"
if [[ ! -f "$package_path" ]]; then
echo "Package does not exist locally:"
echo "$package_path"
exit 1
fi
hash="$(sha256sum "$package_path" | awk '{print $1}')"
mkdir -p "$ROOT/repo/stable"
cat > "$ROOT/repo/stable/manifest.json" <<EOF
{
"version": "$version",
"package": "/packages/$package_name",
"sha256": "$hash",
"requires_reboot": true
}
EOF
else
if [[ ! -f "$ROOT/repo/canary/manifest.json" ]]; then
if [[ ! -f "$RELEASE_LOCAL/canary/manifest.json" ]]; then
echo "No local canary manifest exists."
exit 1
fi
mkdir -p "$ROOT/repo/stable"
cp "$ROOT/repo/canary/manifest.json" "$ROOT/repo/stable/manifest.json"
version="$(python3 -c \
'import json; print(json.load(open("repo/stable/manifest.json"))["version"])')"
'import json, sys; print(json.load(open(sys.argv[1]))["version"])' \
"$RELEASE_LOCAL/canary/manifest.json")"
fi
echo "Promoting $version to STABLE"
release_require_version "$version"
rsync -av \
-e ssh \
"$ROOT/repo/stable/manifest.json" \
classroom-control:/srv/ekklesia/classroom-updates/stable/manifest.json
package_name="$(release_package_name "$version")"
package_path="$RELEASE_LOCAL/packages/$package_name"
if [[ ! -f "$package_path" ]]; then
echo "Package does not exist locally:"
echo "$package_path"
exit 1
fi
package_name="ekklesia-classroom_${version}_amd64.deb"
if [[ "$(release_package_version "$package_path")" != "$version" ]]; then
echo "$package_name says it is $(release_package_version "$package_path")." >&2
exit 1
fi
# THE PERMANENT URL.
#
# .../stable/latest.deb is a symlink beside the manifest pointing at the
# versioned package the manifest names, so there is one download address
# that never changes and still no second place recording which version is
# current. The manifest remains the only answer to that question.
#
# It is swapped AFTER the manifest, and by rename, for two reasons. A
# rename is atomic, so nothing ever downloads a half-written pointer; and
# going last means latest.deb can only ever lag the manifest by a moment,
# never lead it — it can never offer a package the channel has not
# published. The versioned file itself is never touched again once
# uploaded.
ssh classroom-control "
set -e
cd /srv/ekklesia/classroom-updates/stable
test -f ../packages/$package_name
ln -sfn ../packages/$package_name latest.deb.new
mv -Tf latest.deb.new latest.deb
"
# A classroom only ever installs a version newer than its own, so a stable
# manifest that went backwards would reach nobody and would only make the
# channel say something untrue.
current="$(release_channel_version stable || true)"
echo "https://updates.riverofglorymn.org/stable/latest.deb -> $package_name"
if [[ -n "$current" ]] && ! release_version_gt "$version" "$current"; then
echo "Stable is on $current; $version is not newer." >&2
exit 1
fi
hash="$(sha256sum "$package_path" | awk '{print $1}')"
echo "Promoting $version to STABLE (from ${current:-nothing})"
# Refuses unless the server already holds exactly this package.
release_publish_manifest stable "$version" "$hash"
echo "$RELEASE_URL/stable/latest.deb -> $package_name"
echo
release_verify_channel stable "$version"
echo
echo "Ekklesia Classroom $version is now STABLE"
echo "https://updates.riverofglorymn.org/stable/manifest.json"
echo "$RELEASE_URL/stable/manifest.json"