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:
co-authored by
Claude Opus 5.5
parent
6f2d169355
commit
690ef95aff
@@ -235,16 +235,33 @@ check("container service startup is suppressed", "/usr/sbin/policy-rc.d" in vali
|
||||
print()
|
||||
print("6. canary release gate")
|
||||
validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"')
|
||||
package_copy = canary.index('cp "$package_path"')
|
||||
first_rsync = canary.index("rsync -av")
|
||||
package_copy = canary.index('cp -n "$package_path"')
|
||||
first_upload = canary.index("release_upload_package")
|
||||
check("Trixie validation runs before the repository copy", validation < package_copy)
|
||||
check("Trixie validation runs before any upload", validation < first_rsync)
|
||||
check("Trixie validation runs before any upload", validation < first_upload)
|
||||
# A version is checked before anything is built for it, and again after
|
||||
# the long gate, immediately before the first upload.
|
||||
check("a spent version is refused before the build",
|
||||
canary.index("release_check_new_version") < canary.index("build-release.sh"))
|
||||
check("and again after the gate",
|
||||
validation < canary.rindex("release_check_new_version") < first_upload)
|
||||
check("the published channel is read back afterwards",
|
||||
canary.index("release_verify_channel canary") > canary.index("release_publish_manifest canary"))
|
||||
# The gate checks convergence, not just that the package installs: an
|
||||
# upgraded room ends on the v2 channel with every setting this release
|
||||
# has, exactly once, and the command a technician runs is safe to run on
|
||||
# a machine that is already converged.
|
||||
check("the gate checks a deliberate channel survives",
|
||||
"'^CHANNEL=canary$'" in validation_script)
|
||||
'"^CHANNEL=${channel}\\$"' in validation_script)
|
||||
check("and that channel can be stable as well as canary",
|
||||
"VALIDATION_CHANNEL" in validation_script
|
||||
and "stable|canary)" in validation_script)
|
||||
check("the gate checks the installed version is the package's own",
|
||||
"/usr/local/lib/ekklesia-classroom/VERSION" in validation_script)
|
||||
check("and asks the installed updater about it",
|
||||
"ekklesia-classroom-update --check" in validation_script)
|
||||
check("and that an upgrade leaves Presentations off",
|
||||
"grep -q '^PRESENTATIONS_ENABLED=false$'" in validation_script)
|
||||
check("and that the experiment's channel comes home",
|
||||
"'^CHANNEL=stable$'" in validation_script)
|
||||
check("and that every new setting arrived exactly once",
|
||||
@@ -477,27 +494,33 @@ print("7c-iii. the permanent download addresses")
|
||||
# .../stable/latest.deb and .../canary/latest.deb. One address per channel
|
||||
# that never changes, and still no second record of which version is
|
||||
# current: the pointer is a symlink to the package the manifest names.
|
||||
# Both channels publish through one function, so the order below is the
|
||||
# order for both.
|
||||
release_lib = (root / "tools/release-lib.sh").read_text()
|
||||
publish = release_lib[release_lib.index("release_publish_manifest() {"):]
|
||||
publish = publish[:publish.index("\n}\n\n")]
|
||||
publish_code = "\n".join(line for line in publish.splitlines()
|
||||
if not line.lstrip().startswith("#"))
|
||||
for channel, script in (("canary", "update_canary.sh"),
|
||||
("stable", "update_stable.sh")):
|
||||
text = (root / script).read_text()
|
||||
code = "\n".join(line for line in text.splitlines()
|
||||
if not line.lstrip().startswith("#"))
|
||||
check(f"{script} moves the {channel} pointer",
|
||||
f"{channel}/latest.deb" in text or "latest.deb" in code)
|
||||
check(f"{script} points it at the versioned package",
|
||||
"ln -sfn ../packages/$package_name" in code)
|
||||
# Atomic, so nothing downloads a half-written pointer, and after the
|
||||
# manifest, so the pointer can lag it but never lead it — it can never
|
||||
# offer a package the channel has not published.
|
||||
check(f"{script} swaps it by rename rather than in place",
|
||||
"mv -Tf latest.deb.new latest.deb" in code)
|
||||
# Against the rsync that actually uploads the manifest, not the echo
|
||||
# at the end that prints its URL.
|
||||
upload = f"classroom-updates/{channel}/manifest.json"
|
||||
check(f"{script} moves it after the manifest goes up",
|
||||
code.index("latest.deb") > code.index(upload))
|
||||
check(f"{script} refuses to point at a package that is not there",
|
||||
"test -f ../packages/$package_name" in code)
|
||||
check(f"{script} publishes {channel} through the shared function",
|
||||
f"release_publish_manifest {channel} " in text)
|
||||
check("the pointer is moved to the versioned package",
|
||||
"ln -sfn ../packages/$name latest.deb.new" in publish_code)
|
||||
# Atomic, so nothing downloads a half-written pointer, and after the
|
||||
# manifest, so the pointer can lag it but never lead it — it can never
|
||||
# offer a package the channel has not published.
|
||||
check("it is swapped by rename rather than in place",
|
||||
"mv -Tf latest.deb.new latest.deb" in publish_code)
|
||||
check("after the manifest goes up",
|
||||
publish_code.index("latest.deb") > publish_code.index("$channel/manifest.json\""))
|
||||
check("and refuses to point at a package that is not there",
|
||||
"test -f ../packages/$name" in publish_code)
|
||||
check("the manifest is refused unless the server holds exactly that package",
|
||||
publish_code.index("sha256sum") < publish_code.index("cat >"))
|
||||
check("a package upload never replaces a published file",
|
||||
"--ignore-existing" in release_lib)
|
||||
|
||||
check("and neither script writes a version anywhere but the manifest",
|
||||
"latest.json" not in (root / "update_canary.sh").read_text()
|
||||
|
||||
Reference in New Issue
Block a user