From 5d7781c352f1d487d84d1243deab6ef932dd1bde Mon Sep 17 00:00:00 2001 From: ekklesia Date: Tue, 1 Sep 2026 19:15:20 -0500 Subject: [PATCH] chore: checkpoint classroom 0.1.5 stable --- .gitignore | 1 + VERSION | 2 +- config/sway/config | 5 +- packaging/debian/control.template | 9 + packaging/debian/postinst | 38 ++++ scripts/ekklesia-classroom-update | 210 ++++++++++++++++++ .../updater/ekklesia-classroom-update.service | 8 + .../updater/ekklesia-classroom-update.timer | 10 + tools/build-release.sh | 83 +++++++ update_canary.sh | 59 +++++ update_stable.sh | 53 +++++ 11 files changed, 473 insertions(+), 5 deletions(-) create mode 100644 packaging/debian/control.template create mode 100755 packaging/debian/postinst create mode 100755 scripts/ekklesia-classroom-update create mode 100644 systemd/updater/ekklesia-classroom-update.service create mode 100644 systemd/updater/ekklesia-classroom-update.timer create mode 100755 tools/build-release.sh create mode 100755 update_canary.sh create mode 100755 update_stable.sh diff --git a/.gitignore b/.gitignore index 2f2f3b9..b09611d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist/ +repo/ packaging/root/ *.deb diff --git a/VERSION b/VERSION index 6e8bf73..9faa1b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.1.5 diff --git a/config/sway/config b/config/sway/config index 9e9a20a..03b4ea3 100644 --- a/config/sway/config +++ b/config/sway/config @@ -7,10 +7,7 @@ set $mod Mod4 output * bg #202020 solid_color # Waybar -exec_always --no-startup-id pkill waybar -exec_always --no-startup-id waybar \ - -c /etc/ekklesia-classroom/waybar/config.jsonc \ - -s /etc/ekklesia-classroom/waybar/style.css +exec --no-startup-id waybar -c /etc/ekklesia-classroom/waybar/config.jsonc -s /etc/ekklesia-classroom/waybar/style.css # Keep the interface intentionally minimal. # Additional classroom controls are added explicitly by Ekklesia Classroom. diff --git a/packaging/debian/control.template b/packaging/debian/control.template new file mode 100644 index 0000000..c9a640b --- /dev/null +++ b/packaging/debian/control.template @@ -0,0 +1,9 @@ +Package: ekklesia-classroom +Version: @VERSION@ +Section: misc +Priority: optional +Architecture: amd64 +Maintainer: Ekklesia +Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups +Description: Ekklesia managed classroom environment + A minimal Wayland classroom environment for Ekklesia-managed church computers. diff --git a/packaging/debian/postinst b/packaging/debian/postinst new file mode 100755 index 0000000..ed04b9b --- /dev/null +++ b/packaging/debian/postinst @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +if ! id classroom >/dev/null 2>&1; then + adduser \ + --disabled-password \ + --gecos "" \ + classroom +fi + +cat > /home/classroom/.bash_profile <<'PROFILE' +if [ -z "$WAYLAND_DISPLAY" ] && [ "${XDG_VTNR:-}" = "1" ]; then + exec sway -c /etc/ekklesia-classroom/sway/config +fi +PROFILE + +chown classroom:classroom /home/classroom/.bash_profile +chmod 0644 /home/classroom/.bash_profile + +chmod 0755 /usr/local/bin/classroom-browser + +if [ ! -f /etc/ekklesia-classroom/device.conf ]; then + cat > /etc/ekklesia-classroom/device.conf <<'CONFIG' +# Ekklesia Classroom device configuration + +UPDATE_URL=https://updates.riverofglorymn.org +CHANNEL=stable +CONFIG + + chmod 0644 /etc/ekklesia-classroom/device.conf +fi + +systemctl daemon-reload || true + +systemctl enable ekklesia-classroom-update.timer || true +systemctl start ekklesia-classroom-update.timer || true + +exit 0 diff --git a/scripts/ekklesia-classroom-update b/scripts/ekklesia-classroom-update new file mode 100755 index 0000000..790fa44 --- /dev/null +++ b/scripts/ekklesia-classroom-update @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + +import argparse +import fcntl +import hashlib +import json +import os +import subprocess +import sys +import tempfile +import urllib.request +from urllib.parse import urljoin + +CONFIG = "/etc/ekklesia-classroom/device.conf" +LOCKFILE = "/run/ekklesia-classroom-update.lock" + + +def log(message): + print(f"[ekklesia-classroom-update] {message}", flush=True) + + +def read_config(): + config = { + "UPDATE_URL": "https://updates.riverofglorymn.org", + "CHANNEL": "stable", + } + + if os.path.exists(CONFIG): + with open(CONFIG, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + config[key.strip()] = value.strip() + + return config + + +def installed_version(): + result = subprocess.run( + ["dpkg-query", "-W", "-f=${Version}", "ekklesia-classroom"], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + return "0.0.0" + + return result.stdout.strip() + + +def version_is_newer(candidate, current): + result = subprocess.run( + ["dpkg", "--compare-versions", candidate, "gt", current] + ) + return result.returncode == 0 + + +def fetch_json(url): + request = urllib.request.Request( + url, + headers={"User-Agent": "Ekklesia-Classroom-Updater/0.1"}, + ) + + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) + + +def download(url, destination): + request = urllib.request.Request( + url, + headers={"User-Agent": "Ekklesia-Classroom-Updater/0.1"}, + ) + + with urllib.request.urlopen(request, timeout=120) as response: + with open(destination, "wb") as f: + while True: + chunk = response.read(1024 * 1024) + if not chunk: + break + f.write(chunk) + + +def sha256(path): + digest = hashlib.sha256() + + with open(path, "rb") as f: + while True: + chunk = f.read(1024 * 1024) + if not chunk: + break + digest.update(chunk) + + return digest.hexdigest() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--no-reboot", + action="store_true", + help="Install the update but do not reboot automatically", + ) + parser.add_argument( + "--check", + action="store_true", + help="Check for an update without installing it", + ) + args = parser.parse_args() + + with open(LOCKFILE, "w", encoding="utf-8") as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + log("Another update is already running.") + return 0 + + config = read_config() + + base_url = config["UPDATE_URL"].rstrip("/") + "/" + channel = config["CHANNEL"] + manifest_url = urljoin(base_url, f"{channel}/manifest.json") + + current = installed_version() + + log(f"Current version: {current}") + log(f"Channel: {channel}") + log(f"Checking: {manifest_url}") + + try: + manifest = fetch_json(manifest_url) + except Exception as exc: + log(f"Unable to fetch update manifest: {exc}") + return 1 + + candidate = manifest["version"] + + log(f"Available version: {candidate}") + + if not version_is_newer(candidate, current): + log("Already up to date.") + return 0 + + if args.check: + log(f"Update available: {current} -> {candidate}") + return 0 + + package_url = urljoin(base_url, manifest["package"].lstrip("/")) + expected_hash = manifest["sha256"].lower() + + os.makedirs("/var/cache/ekklesia-classroom", exist_ok=True) + + package_path = os.path.join( + "/var/cache/ekklesia-classroom", + os.path.basename(manifest["package"]), + ) + + temporary_path = package_path + ".download" + + log(f"Downloading {package_url}") + + try: + download(package_url, temporary_path) + except Exception as exc: + log(f"Download failed: {exc}") + return 1 + + actual_hash = sha256(temporary_path) + + if actual_hash != expected_hash: + log("SHA-256 verification FAILED.") + log(f"Expected: {expected_hash}") + log(f"Received: {actual_hash}") + os.unlink(temporary_path) + return 1 + + log("SHA-256 verification passed.") + + os.replace(temporary_path, package_path) + + log(f"Installing {candidate}") + + result = subprocess.run( + [ + "apt-get", + "install", + "-y", + package_path, + ] + ) + + if result.returncode != 0: + log("Package installation failed.") + return result.returncode + + log(f"Update to {candidate} installed successfully.") + + if manifest.get("requires_reboot", False): + if args.no_reboot: + log("Update requires reboot, but --no-reboot was specified.") + else: + log("Update requires reboot. Rebooting now.") + subprocess.run(["systemctl", "reboot"]) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/systemd/updater/ekklesia-classroom-update.service b/systemd/updater/ekklesia-classroom-update.service new file mode 100644 index 0000000..e979fda --- /dev/null +++ b/systemd/updater/ekklesia-classroom-update.service @@ -0,0 +1,8 @@ +[Unit] +Description=Ekklesia Classroom Update +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ekklesia-classroom-update diff --git a/systemd/updater/ekklesia-classroom-update.timer b/systemd/updater/ekklesia-classroom-update.timer new file mode 100644 index 0000000..a027827 --- /dev/null +++ b/systemd/updater/ekklesia-classroom-update.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Nightly Ekklesia Classroom Update Check + +[Timer] +OnCalendar=*-*-* 03:00:00 +RandomizedDelaySec=30m +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/tools/build-release.sh b/tools/build-release.sh new file mode 100755 index 0000000..a8bf95f --- /dev/null +++ b/tools/build-release.sh @@ -0,0 +1,83 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 VERSION" + exit 1 +fi + +VERSION="$1" + +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid version: $VERSION" + echo "Expected MAJOR.MINOR.PATCH, for example 0.1.6" + exit 1 +fi + +STAGE="$ROOT/packaging/root" +OUTPUT="$ROOT/dist/ekklesia-classroom_${VERSION}_amd64.deb" + +echo "Building Ekklesia Classroom ${VERSION}" + +# Always construct the package from clean canonical sources. +rm -rf "$STAGE" +mkdir -p "$STAGE/DEBIAN" "$ROOT/dist" + +# Debian package metadata. +sed \ + "s/^Version: @VERSION@$/Version: ${VERSION}/" \ + "$ROOT/packaging/debian/control.template" \ + > "$STAGE/DEBIAN/control" + +install -m755 \ + "$ROOT/packaging/debian/postinst" \ + "$STAGE/DEBIAN/postinst" + +# Classroom UI configuration. +install -Dm644 \ + "$ROOT/config/sway/config" \ + "$STAGE/etc/ekklesia-classroom/sway/config" + +install -Dm644 \ + "$ROOT/config/waybar/config.jsonc" \ + "$STAGE/etc/ekklesia-classroom/waybar/config.jsonc" + +install -Dm644 \ + "$ROOT/config/waybar/style.css" \ + "$STAGE/etc/ekklesia-classroom/waybar/style.css" + +# Classroom commands. +install -Dm755 \ + "$ROOT/scripts/classroom-browser" \ + "$STAGE/usr/local/bin/classroom-browser" + +install -Dm755 \ + "$ROOT/scripts/ekklesia-classroom-update" \ + "$STAGE/usr/local/bin/ekklesia-classroom-update" + +# System configuration. +install -Dm644 \ + "$ROOT/systemd/autologin/autologin.conf" \ + "$STAGE/etc/systemd/system/getty@tty1.service.d/autologin.conf" + +install -Dm644 \ + "$ROOT/systemd/updater/ekklesia-classroom-update.service" \ + "$STAGE/etc/systemd/system/ekklesia-classroom-update.service" + +install -Dm644 \ + "$ROOT/systemd/updater/ekklesia-classroom-update.timer" \ + "$STAGE/etc/systemd/system/ekklesia-classroom-update.timer" + +printf '%s\n' "$VERSION" > "$ROOT/VERSION" + +dpkg-deb \ + --root-owner-group \ + --build "$STAGE" "$OUTPUT" + +echo +echo "Built:" +echo "$OUTPUT" +sha256sum "$OUTPUT" diff --git a/update_canary.sh b/update_canary.sh new file mode 100755 index 0000000..00caf8f --- /dev/null +++ b/update_canary.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +current_version="$(cat VERSION)" + +if [[ $# -eq 1 ]]; then + version="$1" +else + IFS='.' read -r major minor patch <<< "$current_version" + version="${major}.${minor}.$((patch + 1))" +fi + +package_name="ekklesia-classroom_${version}_amd64.deb" +package_path="$ROOT/dist/$package_name" + +echo "Current version: $current_version" +echo "New canary: $version" +echo + +"$ROOT/tools/build-release.sh" "$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" < "$ROOT/repo/stable/manifest.json" <