merge: master is 27-dev; check the next maintenance release
This commit is contained in:
@@ -7,8 +7,47 @@ live, every one of those interrupts every user. An alert people learn to ignore
|
||||
worse than no alert, because one day it carries a security fix.
|
||||
|
||||
## Unreleased
|
||||
### Changed
|
||||
|
||||
- **`master` is now labelled `27-dev`, because it is not the next release.** iX
|
||||
branches each major onto its own `release/` line and master rolls straight on to the
|
||||
one after — on 2026-07-14 every recent commit on master targeted `27.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 to `28-dev` by 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-signatures `restic_backup` and
|
||||
`get_restic_config`, splitting `entry`/`credentials` out of the `cloud_backup` dict.
|
||||
It is deliberately not being chased while the 27 line is still churning.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The next maintenance release was never checked, and it is the one that reaches
|
||||
users.** Shipped versions were discovered from `TS-*` tags and unreleased ones from
|
||||
`release/*` branches carrying `-BETA`/`-RC`. A branched-but-untagged *maintenance*
|
||||
release is neither: `release/25.10.5` has 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.Z` branch 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.2` sorts older than `TS-24.10.2.4` (history, not
|
||||
a warning), and iX's typo branch `release/25.20.2.2` is 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.5` and `release/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`/`-RC` in its name. Otherwise `release/25.10.5` would
|
||||
count as shipped and a break in it would fail the build as a live outage — on a
|
||||
version nobody is running yet.
|
||||
|
||||
- **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_request` key. GitHub omits that key on a plain issue; Gitea sends it as
|
||||
|
||||
@@ -60,8 +60,10 @@ If something is wrong, the reason is in `apply.log` — start at
|
||||
| 24.10.2.4 | ok | ok | — |
|
||||
| 25.04.2.6 | ok | ok | — |
|
||||
| 25.10.4 | ok | ok | v0.7.0: 3 live tasks — 191-dataset nested backup of /mnt/Tap, a 215-filesystem/2-zvol backup of /mnt/Tank/backups, and a non-nested one; 0 orphans, 0 leaked mounts, byte-identical restore; the collector also reclaimed a real orphan the pool had been carrying |
|
||||
| 24.10.2.5 _(unreleased)_ | ok | ok | — |
|
||||
| 25.10.5 _(unreleased)_ | ok | ok | — |
|
||||
| 26.0.0-BETA.3 _(unreleased)_ | ok | ok | — |
|
||||
| master _(unreleased)_ | **BROKEN** | **BROKEN** | — |
|
||||
| master _(27-dev)_ | **BROKEN** | **BROKEN** | — |
|
||||
|
||||
| verdict | meaning |
|
||||
| --- | --- |
|
||||
@@ -72,6 +74,16 @@ If something is wrong, the reason is in `apply.log` — start at
|
||||
"ok" means *the patch's assumptions hold*, checked automatically against iX's
|
||||
source. It does not mean a human ran a backup on it — that is the
|
||||
**Hardware-verified** column, which is filled in by hand and only by doing it.
|
||||
|
||||
**`master` is not the next release.** iX branches each major off to its own
|
||||
`release/` line and master rolls straight on to the one after — so master is
|
||||
`27-dev` while 26 is still in beta. A **BROKEN** master means iX has changed
|
||||
something that will reach users *a major release from now*, not in the version you
|
||||
are about to install. Read the numbered rows for that.
|
||||
|
||||
A row like `25.10.5 _(unreleased)_` is the next maintenance release: branched by iX,
|
||||
not tagged yet, and the very next thing a 25.10.4 box gets. It is checked precisely
|
||||
because it is the one unshipped ref that reaches real users without warning.
|
||||
<!-- END COMPAT MATRIX -->
|
||||
|
||||
The table is **regenerated daily by CI** against iXsystems' actual middleware source
|
||||
|
||||
@@ -629,3 +629,99 @@ class TestTheBotFindsItsOwnIssueOnBOTHForges:
|
||||
{"number": 1, "title": "TypeError when create B2 backup on Electric Eel",
|
||||
"state": "closed", "pull_request": None},
|
||||
]) is None
|
||||
|
||||
|
||||
class TestTheNextMaintenanceReleaseIsChecked:
|
||||
"""`release/25.10.5` fell through every sieve, and it is the one that reaches users.
|
||||
|
||||
Shipped versions come from `TS-*` TAGS; unreleased ones come from `release/*`
|
||||
BRANCHES that carry `-BETA`/`-RC`. A branched-but-untagged MAINTENANCE release is
|
||||
neither: no tag, and its line (25.10) has already shipped, so the "prereleases of
|
||||
a shipped line are history" filter threw it out. It was invisible.
|
||||
|
||||
That is backwards. `release/24.10-RC.2` is history -- nobody can install it. But
|
||||
`release/25.10.5` is the FUTURE of a shipped line: it is what a 25.10.4 box gets
|
||||
on its next update. A break there ships to real users before the daily check has
|
||||
ever looked at it.
|
||||
"""
|
||||
|
||||
TAGS = ["TS-24.10.2.4", "TS-25.04.2.6", "TS-25.10.4"]
|
||||
HEADS = [
|
||||
"release/25.10.4.1",
|
||||
"release/25.10.5", # branched, untagged -- the next maintenance release
|
||||
"release/24.10-RC.2", # history: its line shipped long ago
|
||||
"release/25.20.2.2", # iX's typo branch: 25.20 is not a TrueNAS version
|
||||
"release/26.0.0-BETA.3",
|
||||
"master",
|
||||
]
|
||||
|
||||
def _refs(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
compat, "_ls_remote",
|
||||
lambda remote, what: self.TAGS if what == "--tags" else self.HEADS)
|
||||
return compat.discover_refs("origin")
|
||||
|
||||
def test_the_next_maintenance_release_is_checked(self, monkeypatch):
|
||||
assert "release/25.10.5" in self._refs(monkeypatch), (
|
||||
"the next thing a 25.10.4 box updates to is not checked, so a break in it "
|
||||
"reaches users before the bot ever sees it"
|
||||
)
|
||||
|
||||
def test_a_superseded_maintenance_branch_is_not(self, monkeypatch):
|
||||
# 25.10.4.1 sorts OLDER than the newest tag TS-25.10.4? No -- it is NEWER, and
|
||||
# both are on the 25.10 line, so only the newest branch on the line is taken.
|
||||
refs = self._refs(monkeypatch)
|
||||
assert "release/25.10.4.1" not in refs, "only the newest branch per line"
|
||||
|
||||
def test_the_typo_branch_stays_out(self, monkeypatch):
|
||||
# 25.20 has no TS tag, so it is not a release line at all. A typo branch in the
|
||||
# matrix reads as a real supported release we are silently broken on.
|
||||
assert "release/25.20.2.2" not in self._refs(monkeypatch)
|
||||
|
||||
def test_a_prerelease_of_an_already_shipped_line_stays_out(self, monkeypatch):
|
||||
assert "release/24.10-RC.2" not in self._refs(monkeypatch)
|
||||
|
||||
def test_an_untagged_branch_counts_as_UNRELEASED(self, monkeypatch):
|
||||
# The exit code keys off this. Calling 25.10.5 "shipped" would fail the build
|
||||
# as a live outage on a version nobody is running yet.
|
||||
assert compat.is_unreleased("release/25.10.5")
|
||||
assert compat.is_unreleased("master")
|
||||
assert not compat.is_unreleased("TS-25.10.4")
|
||||
|
||||
|
||||
class TestMasterIsNotTheNextRelease:
|
||||
"""A red `master` row used to read as "the version you are about to install".
|
||||
|
||||
On 2026-07-14 master was 27-dev -- every recent commit targeted 27.0.0-BETA.1 --
|
||||
while 26 was still in beta on its own branches. So `master BROKEN` meant "iX will
|
||||
break us a major release from now", but the matrix said "master _(unreleased)_",
|
||||
which any reader takes as the next thing out the door. For a table whose whole job
|
||||
is helping somebody decide whether to trust this with their backups, that is a
|
||||
false alarm in the worst possible place.
|
||||
"""
|
||||
|
||||
def _rows(self, refs):
|
||||
return [{"ref": r, "unreleased": compat.is_unreleased(r), "modules": {}}
|
||||
for r in refs]
|
||||
|
||||
def test_master_is_labelled_with_the_major_AFTER_the_newest_known_one(self):
|
||||
rows = self._rows(["TS-25.10.4", "release/26.0.0-BETA.3", "master"])
|
||||
assert compat.dev_label(rows) == "27-dev"
|
||||
|
||||
def test_it_rolls_over_on_its_own_when_the_next_beta_branches(self):
|
||||
# Derived, not hardcoded: when release/27.0.0-BETA.1 appears, master is 28-dev.
|
||||
rows = self._rows(["TS-26.0.0", "release/27.0.0-BETA.1", "master"])
|
||||
assert compat.dev_label(rows) == "28-dev"
|
||||
|
||||
def test_the_rendered_matrix_says_dev_not_unreleased(self):
|
||||
healthy = check_files(with_())
|
||||
rows = [
|
||||
{"ref": r, "unreleased": compat.is_unreleased(r), "modules": healthy}
|
||||
for r in ("TS-25.10.4", "release/26.0.0-BETA.3", "master")
|
||||
]
|
||||
md = compat.render_markdown(rows)
|
||||
assert "master _(27-dev)_" in md
|
||||
assert "master _(unreleased)_" not in md
|
||||
# ...and the ordinary rows are untouched.
|
||||
assert "| 25.10.4 |" in md
|
||||
assert "| 26.0.0-BETA.3 _(unreleased)_ |" in md
|
||||
|
||||
+88
-10
@@ -871,6 +871,24 @@ def discover_refs(remote: str = REPO) -> list[str]:
|
||||
* UNRELEASED comes from the BRANCHES, because that is where a beta appears
|
||||
first: `release/26.0.0-BETA.3` had no tag yet while it was the newest beta.
|
||||
Catching breakage here, before it ships, is the whole point of this file.
|
||||
|
||||
THE NEXT MAINTENANCE RELEASE IS ALSO A BRANCH, and it used to fall through both
|
||||
sieves. `release/25.10.5` is branched but not yet tagged, and 25.10 has already
|
||||
shipped -- so it is not in `shipped` (no tag) and it was excluded from `upcoming`
|
||||
(its line is in `shipped_lines`). It was invisible. That is the one ref a 25.10.4
|
||||
user is actually about to be upgraded onto, so a break there reaches people
|
||||
BEFORE the daily check ever looks at it -- the exact hole this file exists to
|
||||
close, on the only line anybody is running.
|
||||
|
||||
The rule that separates it from the two things we must NOT report:
|
||||
|
||||
* `release/24.10-RC.2` -- a prerelease of an already-shipped line. It is
|
||||
history, not a warning; it sorts OLDER than TS-24.10.2.4, so it is dropped.
|
||||
* `release/25.20.2.2` -- iX's typo branch. 25.20 never shipped, so it has no
|
||||
TS tag, so it is not a line at all and is dropped.
|
||||
|
||||
...which is: a plain `release/X.Y.Z` branch counts only if its line HAS shipped
|
||||
and it sorts NEWER than that line's newest tag. Both exclusions fall out of it.
|
||||
"""
|
||||
tags = _ls_remote(remote, "--tags")
|
||||
heads = _ls_remote(remote, "--heads")
|
||||
@@ -878,26 +896,45 @@ def discover_refs(remote: str = REPO) -> list[str]:
|
||||
shipped = _newest_per_line([
|
||||
t for t in tags if t.startswith("TS-") and "-BETA" not in t and "-RC" not in t
|
||||
])
|
||||
newest_shipped = {_version_of(t)[0][:2]: _version_of(t) for t in shipped}
|
||||
|
||||
release_heads = [h for h in heads if h.startswith("release/")]
|
||||
|
||||
# A prerelease of a line that has ALREADY shipped is history, not a warning:
|
||||
# release/24.10-RC.2 still exists, and the nested module does not apply to it,
|
||||
# but 24.10 shipped long ago and TS-24.10.2.4 is fine. Reporting it would be a
|
||||
# standing red row in the matrix for a version nobody can install.
|
||||
shipped_lines = {_version_of(t)[0][:2] for t in shipped}
|
||||
upcoming = [
|
||||
h for h in _newest_per_line([
|
||||
h for h in heads
|
||||
if h.startswith("release/") and ("-BETA" in h or "-RC" in h)
|
||||
h for h in release_heads if "-BETA" in h or "-RC" in h
|
||||
])
|
||||
if _version_of(h)[0][:2] not in shipped_lines
|
||||
if _version_of(h)[0][:2] not in newest_shipped
|
||||
]
|
||||
|
||||
return [*shipped, *upcoming, "master"]
|
||||
# The next maintenance release of a line that HAS shipped: branched, untagged,
|
||||
# and the very next thing those users get. See the docstring.
|
||||
pending = []
|
||||
for h in _newest_per_line([
|
||||
h for h in release_heads if "-BETA" not in h and "-RC" not in h
|
||||
]):
|
||||
v = _version_of(h)
|
||||
tagged = newest_shipped.get(v[0][:2])
|
||||
if tagged and v > tagged:
|
||||
pending.append(h)
|
||||
|
||||
return [*shipped, *pending, *upcoming, "master"]
|
||||
|
||||
|
||||
def is_unreleased(ref: str) -> bool:
|
||||
"""master and any BETA/RC. Breakage here is early warning, not an outage."""
|
||||
return ref == "master" or "-BETA" in ref or "-RC" in ref
|
||||
"""Anything iX has not TAGGED. Breakage here is early warning, not an outage.
|
||||
|
||||
Keyed on where the ref came from, not on its name: `discover_refs` takes shipped
|
||||
releases from `TS-*` TAGS and everything else from BRANCHES, so a `release/*` ref
|
||||
is by construction something iX has not released yet. Testing for `-BETA`/`-RC`
|
||||
instead would call `release/25.10.5` SHIPPED, and a break there would fail the
|
||||
build as a live outage -- on a version nobody is running yet.
|
||||
"""
|
||||
return ref == "master" or ref.startswith("release/")
|
||||
|
||||
|
||||
def matrix(refs=None, remote: str = REPO) -> list[dict]:
|
||||
@@ -963,6 +1000,16 @@ _LEGEND = """
|
||||
"ok" means *the patch's assumptions hold*, checked automatically against iX's
|
||||
source. It does not mean a human ran a backup on it — that is the
|
||||
**Hardware-verified** column, which is filled in by hand and only by doing it.
|
||||
|
||||
**`master` is not the next release.** iX branches each major off to its own
|
||||
`release/` line and master rolls straight on to the one after — so master is
|
||||
`27-dev` while 26 is still in beta. A **BROKEN** master means iX has changed
|
||||
something that will reach users *a major release from now*, not in the version you
|
||||
are about to install. Read the numbered rows for that.
|
||||
|
||||
A row like `25.10.5 _(unreleased)_` is the next maintenance release: branched by iX,
|
||||
not tagged yet, and the very next thing a 25.10.4 box gets. It is checked precisely
|
||||
because it is the one unshipped ref that reaches real users without warning.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1010,17 +1057,42 @@ def update_readme(rows: list[dict], path: str = README) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def dev_label(rows: list[dict]) -> str:
|
||||
"""What `master` is a development line FOR -- e.g. "27-dev".
|
||||
|
||||
`master` is NOT the next release, and labelling it "master _(unreleased)_" said
|
||||
it was. On 2026-07-14 every recent commit on master targeted 27.0.0-BETA.1 while
|
||||
26 was still in beta on its own `release/26.0.0-BETA.*` branches: master had
|
||||
already rolled over to the major AFTER the one that has not shipped yet. So a red
|
||||
`master` row read as "the version you are about to install is broken" when the
|
||||
breakage was a year out, on a line nobody can even download. That is a false alarm
|
||||
aimed squarely at the person deciding whether to trust this with their backups.
|
||||
|
||||
Derived, not hardcoded: the newest major we can see anywhere, plus one. iX branches
|
||||
`release/N.0.0-BETA.1` off master and master immediately becomes N+1 -- so when 27
|
||||
betas appear, this says 28-dev on its own.
|
||||
"""
|
||||
majors = [
|
||||
v[0][0] for v in (_version_of(r["ref"]) for r in rows if r["ref"] != "master")
|
||||
if v
|
||||
]
|
||||
return f"{max(majors) + 1}-dev" if majors else "unreleased"
|
||||
|
||||
|
||||
def render_markdown(rows: list[dict]) -> str:
|
||||
"""The matrix, for the README."""
|
||||
out = [
|
||||
"| TrueNAS | B2/S3 providers | Nested snapshots | Hardware-verified |",
|
||||
"| --- | --- | --- | --- |",
|
||||
]
|
||||
dev = dev_label(rows)
|
||||
for row in rows:
|
||||
m = row["modules"]
|
||||
ref = row["ref"]
|
||||
label = ref.removeprefix("TS-").removeprefix("release/")
|
||||
if row["unreleased"]:
|
||||
if ref == "master":
|
||||
label = f"master _({dev})_"
|
||||
elif row["unreleased"]:
|
||||
label = f"{label} _(unreleased)_"
|
||||
|
||||
cells = []
|
||||
@@ -1138,7 +1210,13 @@ def render_matrix(rows: list[dict]) -> str:
|
||||
hardware-verified column lives in COMPATIBILITY.md and is maintained by hand,
|
||||
because nothing else can honestly fill it in.
|
||||
"""
|
||||
w = max((len(r["ref"]) for r in rows), default=10)
|
||||
dev = dev_label(rows)
|
||||
# Same relabel as the README: a red `master` is a warning about the major AFTER
|
||||
# next, and "master" alone reads as "the release you are about to install".
|
||||
names = {r["ref"]: (f"master ({dev})" if r["ref"] == "master" else r["ref"])
|
||||
for r in rows}
|
||||
|
||||
w = max((len(n) for n in names.values()), default=10)
|
||||
lines = [
|
||||
f"{'TrueNAS'.ljust(w)} {'providers':<10} {'nested':<10}",
|
||||
f"{'-' * w} {'-' * 10} {'-' * 10}",
|
||||
@@ -1146,7 +1224,7 @@ def render_matrix(rows: list[dict]) -> str:
|
||||
for row in rows:
|
||||
m = row["modules"]
|
||||
lines.append(
|
||||
f"{row['ref'].ljust(w)} "
|
||||
f"{names[row['ref']].ljust(w)} "
|
||||
f"{_verdict(m[PROVIDERS]):<10} {_verdict(m[NESTED]):<10}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user