-
v0.8.0-rc3
Pre-ReleaseTrueNAS compatibility / compat (push) Successful in 15sRelease / release (push) Successful in 15sCI / python 3.11 (push) Failing after 7sCI / python 3.12 (push) Successful in 17sCI / python 3.13 (push) Successful in 20sCI / shell (shellcheck + syntax) (push) Successful in 9sreleased this
2026-08-26 01:20:27 -04:00 | 2 commits to main since this releaseChanged
-
CI's python matrix is green on the self-hosted Gitea runner again. The real
failure was that the Gitea runner image executes jobs as root, and the two
unreadable-sidecar tests build their scenario withchmod(0)— which cannot make
a file unreadable for root (CAP_DAC_OVERRIDE). Those two tests now skip as root
with that reason; GitHub's non-root runner still exercises them. The matrix also
moved to uv-managed interpreters (one toolchain across both runners) and ruff is
pinned to 0.16.1 so an upstream ruff release can't turnmainred without a code
change. -
README badges point at the public GitHub mirror (workflow status and
releases) instead of the private forge. The release badge had also been reading
the stale Gitea v0.6.1 release instead of the current v0.7.0 on GitHub. -
masteris now labelled27-dev, because it is not the next release. iX
branches each major onto its ownrelease/line and master rolls straight on to the
one after — on 2026-07-14 every recent commit on master targeted27.0.0-BETA.1
while 26 was still in beta. So a BROKEN master row, rendered as
"master (unreleased)", read as "the version you are about to install is broken"
when the breakage was a major release away on a line nobody can download. In a table
whose entire job is helping somebody decide whether to trust this with their backups,
that is a false alarm in the worst possible place. The label is derived from the
newest major in the matrix plus one, so it rolls over to28-devby itself once 27
branches.For the record, the breakage is
NAS-141498(2026-06-24), "Convert cloud_backup
plugin to the typesafe pattern": it re-signaturesrestic_backupand
get_restic_config, splittingentry/credentialsout of thecloud_backupdict.
It is deliberately not being chased while the 27 line is still churning.
Fixed
-
The patch survived being applied and then silently stopped existing, because
something else remounted/usrfour seconds later. On a box running
TrueNAS 25.10.6 the boot of 2026-08-19 went: 16:41:56apply.shmounts its
overlay on/usr/lib/python3/dist-packages, patchesb2.py/restic.py, logs
every stepOK; 16:42:00 a second PREINIT hook runssystemd-sysext refreshover/usr—Unmerged '/usr'./Merged extensions into '/usr'.—
and our overlay, which lives inside that hierarchy, is torn off with it;
16:47:24 our own deferred restart fires exactly as designed and middlewared
imports the stock modules. Every B2 TrueCloud Backup task then failed with
NotImplementedErrorfrom stockrclone/base.pyfor nineteen hours, across
four scheduled runs, whileapply.logandhook_status.jsonboth said the
patch was active.Nothing in the patch was wrong, which is the point: applying at PREINIT and
restarting later is only sound if the patched files are still on the live path
when middlewared re-imports them, and that is not something PREINIT can
guarantee. Init scripts run sequentially in id order, so any hook registered
after ours always wins. Worse, hook ordering cannot fix it either —
middlewared's owndocker.configure_nvidiamerges a sysext over/usrat
runtime, long after every PREINIT hook is finished.So the deferred restart no longer trusts the PREINIT pass.
wait_restart.sh
now re-applies immediately before it restarts middlewared — after boot has
settled, which is also after every sysext merge and docker nvidia
configuration — and verifies the marker is genuinely on the live path before
restarting. It is no longerexec systemctl try-restart middlewared, because
something has to run afterwards.What runs afterwards deliberately does not restart again.
try-restart
returns as soon as middlewared is READY, and middlewared then brings docker up
—docker.configure_nvidiamerges the stock nvidia sysext over/usrat that
point, detaching the overlay after the patched modules have already been
imported. A disk check there reports "missing" on a perfectly healthy system,
and restarting on that signal would restart a correctly-patched middlewared
straight back into the same race. So the overlay is re-mounted for the benefit
of the next restart, and the question of whether this middlewared actually
holds the patch is left to the one thing that can answer it exactly — the
in-process alert below. That re-mount preserveshook_status.json's
patched_at:create_task.py verifydecides "loaded" by comparing
middlewared's start time against that stamp, so a re-apply running after the
restart would have made the stamp newer than the process which correctly
imported the patch, andverifywould have reported FAIL forever on every
boot where the sysext merge detaches the overlay. Caught on hardware while
validating the candidate — a new lying status introduced by the fix for a
lying status.Two supporting fixes fell out of the same failure.
_ensure_writabletreated
"one of our overlays is listed on this directory" as "already done" — but it
only ever reaches that check when the directory is not writable, and a live
overlay of ours always is. A shadowed overlay was therefore indistinguishable
from a healthy one; it is now detached and re-mounted, reusing the same
upperdir so everything patched earlier in the boot reappears intact, with a
fresh workdir because overlayfs refuses one left behind by a detached mount. -
middlewared now says so when it is running stock. The gap that let this
cost nineteen hours was not the remount, it was that nothing could tell the
difference between "patched on disk" and "patched in the running process".
apply.logcan only ever report the first. A new CRITICAL alert asks the
second question from inside middlewared, hourly, where it is exact: the patch
stamps the objects it replaces, so a missing stamp means this interpreter
imported stock code. It checks both halves —restic.py's_truecloud_patched
marker and whetherB2RcloneRemote.get_restic_configis still the base class's
— since either can go missing alone. It stays quiet when the kill switch is
set or the providers module has been retired as native, and it is deliberately
not silenced byupdate_alerts_disabled: that mutes release notifications,
not a broken backup path.Boot-time diagnosis also no longer depends on the journal.
wait_restart.sh
logged only to the journal, and journald retention on a busy box is easily
shorter than the interval between reboots — the 2026-08-19 boot had already
rotated away by the time it was investigated. It now writes toapply.log
alongside everything else. -
The next maintenance release was never checked, and it is the one that reaches
users. Shipped versions were discovered fromTS-*tags and unreleased ones from
release/*branches carrying-BETA/-RC. A branched-but-untagged maintenance
release is neither:release/25.10.5has no tag, and its line has already shipped,
so the "a prerelease of a shipped line is history" filter discarded it. It was
invisible — and it is precisely what a 25.10.4 box gets on its next update. A break
there would have reached real users before the daily check ever looked at it, on the
only line anybody is actually running.A plain
release/X.Y.Zbranch is now checked when its line has shipped and it
sorts newer than that line's newest tag. Both things that must stay out fall out
of the same rule:release/24.10-RC.2sorts older thanTS-24.10.2.4(history, not
a warning), and iX's typo branchrelease/25.20.2.2is on a line that has no tag at
all, so it is not a release line. This immediately surfaced two refs that had never
been checked —release/25.10.5andrelease/24.10.2.5— both of which pass.is_unreleased()now keys off where a ref came from (branch = not yet shipped)
rather than looking for-BETA/-RCin its name. Otherwiserelease/25.10.5would
count as shipped and a break in it would fail the build as a live outage — on a
version nobody is running yet. -
An unchanged fingerprint froze the bug report's body, not just its comments. Two
questions were sharing one answer. Have the findings changed? gates comments —
they notify, and a daily "still broken, same as yesterday" is what teaches everyone
to ignore the one that finally matters. Is the body still true? gates the body —
and editing an issue body notifies nobody on either forge, so keeping it honest is
free. Conflated, the report could never be corrected while the findings held steady,
and the fingerprint deliberately ignores everything that moves on its own — healthy
rows, the hardware-verified column, point releases, and how a row is labelled. The
master→27-devrelabel above would have reached the README and never the issue
anybody actually opens. The body is now rewritten whenever it is out of date (after
normalising line endings, so a forge round-tripping\r\ndoes not cause a rewrite
every run) and comments remain strictly a changelog of real changes. -
A change to the publisher did not re-run the check.
compat.yml'spush:paths
listedtools/compat.pybut nottools/compat_publish.py— so the very commit that
taught the bot to refresh a stale report body triggered no run, and the report stayed
stale until the next scheduled one. A fix nobody runs is a fix nobody has. -
The compatibility bot filed a new duplicate bug report on every Gitea run.
find_issue()skipped pull requests by testing for the presence of the
pull_requestkey. GitHub omits that key on a plain issue; Gitea sends it as
null. So on Gitea every issue was discarded as a PR, the lookup always came back
empty, and the bot took the "nothing filed yet" branch and opened a fresh report
each run — nine copies on the canonical forge, four of them filed after the
commit that was meant to stop precisely this. The mirror was fine, which is why it
went unnoticed: GitHub's payload shape is the one the filter was written against.It is the same failure the anti-spam fix was written to prevent, moved from
comments to issues, and it survived becausefind_issuewas the only function in
compat_publish.pywith no test. It now has one, per forge, and the daily cron —
which had not yet run once — no longer accumulates a report a day.The issue list is also requested with both paging parameters (
per_pagefor
GitHub,limitfor Gitea). Each forge ignores the other's, and Gitea's default page
is 30, so the lookup would have started missing the report again once the pile it
was creating grew past one page.
Downloads
-