Files
truenas-truecloud-patch/.github/workflows/compat.yml
T
flan 82084b6806
CI / shell (shellcheck + syntax) (push) Successful in 10s
CI / python 3.11 (push) Successful in 14s
CI / python 3.12 (push) Successful in 17s
CI / python 3.13 (push) Successful in 17s
TrueNAS compatibility / compat (push) Successful in 11s
The bug-report bot was spamming; make it say something only when there is something to say
It commented on every run that found a break. In one day it left ELEVEN identical
3,000-character comments on the same issue. That is not a warning system, it is a mute
button with extra steps -- and the next real finding would have been scrolled past,
which defeats the entire reason for building it.

Now: the issue BODY is the current truth, edited in place. COMMENTS are a changelog of
changes. A fingerprint of the findings (broken ref/module/problem triples only) is
embedded in the body; a run whose findings match it says nothing at all. It closes the
issue when everything is fixed.

The fingerprint deliberately ignores anything that moves on its own -- healthy rows,
the hardware-verified column, TrueNAS point releases -- so TS-25.10.4 becoming
TS-25.10.5 is not news and does not wake anybody up.

Also:
- The two near-identical per-forge shell steps are gone, replaced by one tested
  implementation (tools/compat_publish.py). Two copies of 'find the issue, decide
  whether to comment' is two chances to drift, and the Gitea one duplicated an issue
  for real.
- The README matrix refresh now opens a PULL REQUEST instead of pushing straight to
  main from CI. An unattended push to main is exactly what the release barrier exists
  to prevent: a bot that can move main can move it somewhere nobody looked.
2026-07-13 20:20:35 +00:00

199 lines
8.3 KiB
YAML

name: TrueNAS compatibility
# Find out that iX broke us BEFORE their release ships, not after a user's backup
# fails.
#
# This patch appends code to middlewared's internal modules. There is no stability
# contract: TrueNAS 26 rewrote the whole cloud_backup path from async to sync, and
# every block the nested module injects is an `async def` wrapping an `await`ed
# original. Nobody would have found out until a restore did not work.
#
# tools/compat.py records what the patch assumes and checks it against iX's actual
# source at every release line -- including master and the current BETA/RC, which is
# where a break shows up first. When an UNRELEASED line breaks, this opens a bug
# report so there is time to fix it before that version reaches anyone.
#
# Runs on both forges: Gitea (canonical) and GitHub (mirror). Only the "file an
# issue" call differs.
on:
schedule:
- cron: "17 6 * * *" # daily, off the hour: everyone crons on the hour
workflow_dispatch:
push:
paths:
# The manifest itself changed -- re-check immediately rather than waiting a day.
- "tools/compat.py"
- ".github/workflows/compat.yml"
permissions:
contents: read
issues: write
jobs:
compat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
# ONE pass over the network. Every other step renders from this JSON -- calling
# compat.py three times would re-fetch every file from every release line three
# times, and could even disagree with itself if iX pushed mid-run.
#
# The exit code is CAPTURED, not allowed to abort the step: a nonzero exit means
# "a shipped release is broken", which is a result to report, not a reason to
# die before reporting it. (Actions runs `bash -e`, so `cmd > out` followed by
# `echo $?` never reaches the echo.) It becomes a job failure at the end, after
# the bug report has been filed.
- name: check every TrueNAS release line
id: check
run: |
rc=0
python3 tools/compat.py --matrix --json > /tmp/matrix.json || rc=$?
echo "shipped_broken=$rc" >> "$GITHUB_OUTPUT"
# The report body is written to a FILE, and never becomes a step output.
#
# An earlier version did `echo "${{ steps.report.outputs.body }}"`, which
# splices the text into the shell script itself -- and the report is full of
# backticks, so bash ran `create-snapshot`, `def` and `async` as commands. It
# is also an injection vector: the report is built from iX's source, so
# anything that lands in middleware would execute on the runner.
#
# The rule that avoids the whole class: never interpolate ${{ }} into a `run:`
# body. Files for data, `env:` for scalars (the runner sets those, rather than
# pasting them into the script).
- name: build the report
id: report
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json, sys
sys.path.insert(0, "tools")
import compat
with open("/tmp/matrix.json") as fh:
rows = json.load(fh)
with open("/tmp/matrix.md", "w") as fh:
fh.write(compat.render_markdown(rows))
broken = [
r for r in rows
if any(compat.is_broken(m) for m in r["modules"].values())
]
native = [
(r["ref"], mod)
for r in rows
for mod, m in sorted(r["modules"].items())
if m["native"] and not compat.is_broken(m)
]
lines = [
"`tools/compat.py` found that the patch's assumptions about "
"middlewared no longer hold.",
"",
compat.render_markdown(rows),
"",
]
for r in broken:
lines.append(f"### {r['ref']}")
lines.append("")
for mod, m in sorted(r["modules"].items()):
if not compat.is_broken(m):
continue
lines.append(f"**{mod}** — the patch will not apply:")
lines.append("")
for p in m["problems"]:
lines.append(f"- `{p['id']}`: {p['detail']}")
lines.append(f" - why it matters: {p['why']}")
lines.append("")
for ref, mod in native:
lines.append(
f"- `{ref}`: **{mod}** appears to be NATIVE now — retire the "
f"module rather than fixing it."
)
lines += ["", "_Filed automatically by `.github/workflows/compat.yml`._"]
with open("/tmp/issue.md", "w") as fh:
fh.write("\n".join(lines))
# Scalars only. The body stays in the file.
print(f"broken={'1' if broken else '0'}")
print(f"refs={','.join(r['ref'] for r in broken)}")
PY
- name: matrix
run: cat /tmp/matrix.md
# Keep the README's table true — but as a PULL REQUEST, not a push to main.
#
# This used to `git push origin HEAD:main` from CI. Unattended writes to main are
# exactly what the release barrier exists to prevent; a bot that can move main can
# move it somewhere nobody looked. A PR is reviewable, and nothing lands by itself.
#
# A stale support matrix is not a stale doc — it is a false promise to somebody
# deciding whether to trust this with their backups. So it still gets refreshed
# daily; it just asks first.
- name: refresh the README matrix (as a PR)
if: ${{ github.event_name == 'schedule' && contains(github.server_url, 'github.com') }}
env:
GH_TOKEN: ${{ github.token }}
run: |
python3 - <<'PY'
import json, sys
sys.path.insert(0, "tools")
import compat
with open("/tmp/matrix.json") as fh:
rows = json.load(fh)
print("changed" if compat.update_readme(rows) else "unchanged")
PY
git diff --quiet -- README.md && { echo "matrix unchanged"; exit 0; }
git config user.name "truecloud-patch bot"
git config user.email "bot@onetick.ninja"
branch="bot/compat-matrix"
git checkout -B "$branch"
git add README.md
git commit -m "docs: refresh the TrueNAS compatibility matrix"
git push -f origin "$branch"
# One long-lived PR, updated in place — not a new one every morning.
if ! gh pr view "$branch" --json number >/dev/null 2>&1; then
gh pr create --head "$branch" --base main \
--title "docs: refresh the TrueNAS compatibility matrix" \
--body "The daily compatibility check found that the support matrix in the README no longer matches what iXsystems' middleware actually looks like.
This PR only touches the block between the \`COMPAT MATRIX\` markers. It is regenerated by \`tools/compat.py --matrix --update-readme\` and force-pushed, so it always reflects the latest run."
fi
# ONE bug report, kept in sync. It is edited in place when the findings change and
# says NOTHING when they do not.
#
# The first version commented on every run and left 11 identical 3,000-character
# comments on one issue in a single day. A bot that repeats itself daily gets
# muted, and then the next real finding is scrolled past — which defeats the whole
# reason for building it.
#
# Runs on whichever forge it lands on; compat_publish.py handles both, so the two
# cannot drift.
- name: file / update / close the bug report
env:
TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
API: ${{ contains(github.server_url, 'github.com') && 'https://api.github.com' || format('{0}/api/v1', github.server_url) }}/repos/${{ github.repository }}
run: python3 tools/compat_publish.py --api "$API" --token "$TOKEN" --matrix /tmp/matrix.json
# A broken SHIPPED release is an outage: users are on it right now. Fails LAST, so
# the report is filed before the job goes red.
- name: fail if a shipped release is broken
if: ${{ steps.check.outputs.shipped_broken != '0' }}
run: |
echo "::error::The patch is broken on a SHIPPED TrueNAS release."
exit 1