ci: add the upgrade-smoke job (a release must install on top of the running one) - #4
Merged
Merged
Conversation
added 7 commits
September 22, 2026 19:17
A release must still install cleanly on top of the version a school is already running. The new job installs the newest published release (or the tag given as the prev_tag dispatch input), creates the state an admin has on a running server -- the three secret files, the EAP CA, an instance record and its server certificate -- upgrades to the freshly built package and then proves that nothing was lost: config.yml, the secrets, the certificates and the rendered instance.d configs byte-identical, permissions and ownership unchanged, every admin-set field of the instance record intact, the service still enabled and active, the venv metadata equal to the dpkg version and no leftovers of the old version under /opt. The instance record is compared field by field instead of by checksum on purpose: 7.3.1 legitimately added ldap_ca to the model and the postinst's update-all rewrites image, so a blanket sha256 would be a false alarm. apt installs the built package with --reinstall --allow-downgrades so the version-equal case after a release still unpacks over the installed version and really runs the postinst upgrade branch; the job asserts both the dpkg "over (<old>)" line and the service restart that only that branch performs. ci.yml no longer runs on every branch push: pull requests cover branches, so each commit was checked twice.
The venv ships four symlinks (bin/python, bin/python3, bin/python3.12, lib64) and a symlink is always lrwxrwxrwx -- chmod cannot change that, so the postinst's `chmod -R go-w` leaves them as they are and the check counted four false positives. Only files and directories are checked now.
…cord The field-by-field comparison deliberately ignores keys the software owns, which means a newly added field escapes it unnoticed. Print the raw diff of the instance record next to it (never fatal): across a version jump it shows exactly which keys the software rewrote -- today ldap_ca, added in 7.3.1, and image, rewritten and rolled back by the postinst's update-all -- so the next added field gets into the key list in the same PR.
Three hardenings, two of them from the squid rollout: The fixture instance now pins the data-plane image of the previous package line instead of letting the record inherit the default. update_all() skips every instance that is already on the target image, so with the default pinned the postinst's update path silently did nothing on a version-adjacent upgrade and every assertion passed without it having run. The job asserts that the fixture image differs from DEFAULT_IMAGE, and that the journal shows update-all reaching the instance -- its outcome stays unasserted, the container cannot become healthy without a DC. The released version's CLI is driven from the venv rather than /usr/bin: v0.1.0..v0.1.3 shipped without that symlink, and an old prev_tag must not fall over a missing one. That the new version puts it back is now asserted after the upgrade.
…pgrade twice Review findings on this branch plus the two template changes made after it. The checksum manifest could run empty and still pass: without -r, xargs runs sha256sum once with no arguments, it hashes stdin, and `sha256sum -c` then reports "-: OK" with exit 0. The manifest now uses `xargs -r` and is pinned to exactly the eleven files the fixture creates, so a renamed path or a fixture that silently did not run fails instead of passing green. Skipping is now only legitimate when the repo has no release at all: the tag selection falls back to the newest prerelease, and it refuses to skip when releases exist. upgrade-smoke gates the release job, so a silent skip would have published a tag whose upgrade path was never tested. The upgrade is proven twice: apt's "Unpacking ... over (...)" line and dpkg's own log entry, which does not depend on apt's wording. The update-all assertion reads the journal from the start of the upgrade instead of the whole boot, so a future fixture step logging the same line cannot satisfy it.
This job checks the repository out, so the working directory is on sys.path for every python it starts. A module could then be imported from the source tree instead of from the installed package, and the version and DEFAULT_IMAGE assertions would test the checkout rather than the upgrade. Every python in the job now runs with -P. The paths happen to be safe today -- lmnradius lives under controlplane/, not at the repository root -- so this closes the hazard, not an observed failure.
…olation
A ${{ }} expression inside a run block is substituted before the shell sees
it, so the value of prev_tag became part of the script text. The step now
carries it as PREV_TAG under env: and the script reads tag="${PREV_TAG:-}".
Follows the hub template.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
ci.ymlandrelease.ymlget a new jobupgrade-smoke, taken from the hubtemplate (
linuxmusterDEV/templates/{ci,release}.yml) and filled with thispackage's real paths. In
release.ymlit is also aneeds:of thereleasejob, so a tag cannot produce a release that fails the upgrade path.
ci.ymlno longer runs on every branch push (push.branches: [main], tagsunchanged): pull requests already cover branches, so until now every commit ran
the whole pipeline twice.
debian/changeloggains the 7.3.2 entry describing the new guard.Why
Kevin's core requirement: a new version must still install cleanly on servers
that already run an older one. Until now CI only ever proved that the package
installs on a clean Ubuntu 24.04 — the interesting failure (config, secrets,
certificates or the instance record lost or clobbered on upgrade) was never
exercised by any pipeline.
How it works
gh release listpicks the newest non-draft, non-prerelease tag — theversion a school is actually running — and downloads its
.deb(3 retries). The
prev_tagdispatch input overrides it for a deliberatejump across an older line. With no release at all the job skips itself.
docs/install.mdproduces on a real server: the three secret files (
join.authfile,ldap-bind.secret,radius.secret, ownerlmnradius, mode 0600), the EAPCA (
lmnradius ca init, pure crypto — no DC, no Docker), an instance record(
lmnradius create --name ci …) and its EAP server certificate(
lmnradius cert issue ci).createis|| true: the container cannot come up without a DC, butReconciler.apply()stores the record and rendersinstance.d/beforeDocker is touched, which is exactly the state under test. The instance uses
ldap://, notldaps://, because from 7.3.1 on an LDAPS instance must pina CA with
--ldap-ca— a flag the older releases this job can upgrade fromdo not have.
apt-get install --reinstall --allow-downgrades. Without those flags theversion-equal case right after a release would report "already the newest
version" and test nothing; with them dpkg unpacks over the installed version
and sets
$2in the postinst, so the upgrade branches really run.config.yml(the API token!),secrets/*,certs/**andinstance.d/**byte-identical (sha256sum -c);find -printf '%m %U:%G %p'diff):secrets/andcerts/stay 0700, secrets and private keys 0600;legitimately added
ldap_cato the model and the postinst'supdate-allrewrites
image, so a blanket sha256 would be a false alarm. The list ofadmin-set keys lives in the job; whoever adds such a field extends it in
the same PR;
enabledandactive, control-plane API answering;importlib.metadataversion equals the dpkg version;/opt: nothing dpkg does not own, only the newlmnradius-<version>.dist-info, onlycpython-312bytecode, nothingworld-writable;
dpkg --auditsilent,apt-get -f install -sclean,dpkg --verifysilent — guarded by a check that
md5sumsactually exists, because up to7.3.0 the package shipped none and
dpkg --verifywas a no-op.Proof that the upgrade path really ran (not a silent no-op)
Two independent assertions in the job itself:
grep -qE 'Unpacking linuxmuster-radius \([^)]+\) over \([^)]+\)'on the aptoutput;
MainPIDmust change.prermdeliberately leaves the servicerunning on upgrade, and the only thing that restarts it is
systemctl try-restartinside the postinst'sif [ -n "${2:-}" ]branch — which dpkgenters only when it passes an old version.
Making sure the upgrade path is not skipped
update_all()skips every instance that is already on the target image, so ifthe fixture instance inherited the package default, the postinst's update path
would quietly do nothing on a version-adjacent upgrade and all assertions below
would pass without it. The fixture therefore pins the data-plane image of the
previous package line — what an instance created by an earlier
.debreallyruns — the job asserts that it differs from
DEFAULT_IMAGE, and it asserts thatthe journal shows
update-allreaching the instance. Its outcome staysunasserted on purpose: without a DC the container cannot become healthy, the
postinst rolls it back and ignores the result.
The released version's CLI is driven from
/opt/linuxmuster-radius/venv/binrather than
/usr/bin: v0.1.0..v0.1.3 shipped without that symlink, and an oldprev_tagmust not fall over a missing one. That the new version puts it backis asserted after the upgrade.
How tested
https://github.com/faircomp/linuxmuster-radius/actions/runs/35761821314
OKundersha256sum -c, permissions diffempty, instance record diff empty.
prev_tag=v0.1.6— the jump across the pre-conventionsline — also green:
https://github.com/faircomp/linuxmuster-radius/actions/runs/35762111067
blanket checksum would have failed on:
ubuntu-24.04.What this cannot prove
Nothing about WLAN authentication: no DC, no winbind trust, no
eapol_test.The data-plane container is intentionally never asserted on (it cannot become
healthy without a domain). What is guarded here is the package and data level:
that an upgrade does not lose or clobber configuration, secrets, certificates
or the instance record.