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
+43 -57
View File
@@ -4,32 +4,50 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
current_version="$(cat VERSION)"
source "$ROOT/tools/release-lib.sh"
if [[ $# -eq 1 ]]; then
version="$1"
else
IFS='.' read -r major minor patch <<< "$current_version"
version="${major}.${minor}.$((patch + 1))"
# The version is always said out loud.
#
# With only three parts there was one obvious next number. With four
# there are two — 0.2.6 is followed by 0.2.6.1 if it is a fix and by
# 0.2.7 if it is not — and which one a release is, is a decision about
# what is in it, not arithmetic.
if [[ $# -ne 1 ]]; then
current="$(release_channel_version canary || true)"
echo "Usage: $0 VERSION" >&2
echo >&2
echo "Canary is on ${current:-an unknown version}. Name the release, for example:" >&2
if [[ -n "$current" ]] && release_version_valid "$current"; then
IFS='.' read -r major minor patch maintenance <<< "$current"
echo " $0 ${major}.${minor}.${patch}.$(( ${maintenance:-0} + 1 )) a maintenance release" >&2
echo " $0 ${major}.${minor}.$(( patch + 1 )) the next feature release" >&2
fi
exit 2
fi
package_name="ekklesia-classroom_${version}_amd64.deb"
version="$1"
package_name="$(release_package_name "$version")"
package_path="$ROOT/dist/$package_name"
echo "Current version: $current_version"
echo "Current canary: $(release_channel_version canary || echo unknown)"
echo "New canary: $version"
echo
# Before anything is built: a version that has been published, or that
# would not be newer than everything that has been, stops here.
release_check_new_version "$version"
"$ROOT/tools/build-release.sh" "$version"
# Validate the exact artifact that will be published. If a previous canary is
# available locally, exercise the real upgrade path as well as a fresh install.
validation_args=("$package_path")
if [[ -f "$ROOT/repo/canary/manifest.json" ]]; then
if [[ -f "$RELEASE_LOCAL/canary/manifest.json" ]]; then
previous_package_name="$(python3 -c \
'import json; print(json.load(open("repo/canary/manifest.json"))["package"].rsplit("/", 1)[-1])')"
previous_package="$ROOT/repo/packages/$previous_package_name"
'import json, sys; print(json.load(open(sys.argv[1]))["package"].rsplit("/", 1)[-1])' \
"$RELEASE_LOCAL/canary/manifest.json")"
previous_package="$RELEASE_LOCAL/packages/$previous_package_name"
if [[ -f "$previous_package" && "$previous_package" != "$package_path" ]]; then
validation_args+=("$previous_package")
@@ -38,62 +56,30 @@ fi
"$ROOT/tools/validate-trixie-release.sh" "${validation_args[@]}"
# Checked again after the gate, which takes long enough for somebody else
# to have published in the meantime.
release_check_new_version "$version"
hash="$(sha256sum "$package_path" | awk '{print $1}')"
mkdir -p "$ROOT/repo/packages" "$ROOT/repo/canary"
cp "$package_path" "$ROOT/repo/packages/$package_name"
cat > "$ROOT/repo/canary/manifest.json" <<EOF
{
"version": "$version",
"package": "/packages/$package_name",
"sha256": "$hash",
"requires_reboot": true
}
EOF
mkdir -p "$RELEASE_LOCAL/packages"
cp -n "$package_path" "$RELEASE_LOCAL/packages/$package_name"
echo
echo "Publishing package..."
rsync -av \
-e ssh \
"$ROOT/repo/packages/$package_name" \
classroom-control:/srv/ekklesia/classroom-updates/packages/
release_upload_package "$RELEASE_LOCAL/packages/$package_name"
echo
echo "Publishing canary manifest..."
release_publish_manifest canary "$version" "$hash"
# Manifest deliberately goes LAST.
rsync -av \
-e ssh \
"$ROOT/repo/canary/manifest.json" \
classroom-control:/srv/ekklesia/classroom-updates/canary/manifest.json
echo "$RELEASE_URL/canary/latest.deb -> $package_name"
# THE PERMANENT URL.
#
# .../canary/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/canary
test -f ../packages/$package_name
ln -sfn ../packages/$package_name latest.deb.new
mv -Tf latest.deb.new latest.deb
"
echo "https://updates.riverofglorymn.org/canary/latest.deb -> $package_name"
echo
echo "Checking what a classroom will see..."
release_verify_channel canary "$version"
echo " stable is still on $(release_channel_version stable || echo unknown)"
echo
echo "Published Ekklesia Classroom $version to CANARY"
echo "https://updates.riverofglorymn.org/canary/manifest.json"
echo "$RELEASE_URL/canary/manifest.json"