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.
This commit is contained in:
@@ -130,17 +130,19 @@ jobs:
|
||||
- name: matrix
|
||||
run: cat /tmp/matrix.md
|
||||
|
||||
# Keep the README's table true. A support matrix that quietly goes stale is not
|
||||
# a stale doc -- it is a false promise to somebody deciding whether to trust
|
||||
# this with their backups.
|
||||
# Keep the README's table true — but as a PULL REQUEST, not a push to main.
|
||||
#
|
||||
# Only ever touches the block between the COMPAT MATRIX markers, and only on
|
||||
# the canonical host (Gitea) so the two forges cannot race each other. The
|
||||
# `paths:` trigger above does not include README.md, so this cannot re-trigger
|
||||
# itself; and a README change is documentation-only, which by design raises no
|
||||
# update alert on anyone's box.
|
||||
- name: refresh the README matrix
|
||||
if: ${{ github.event_name == 'schedule' && !contains(github.server_url, 'github.com') }}
|
||||
# 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
|
||||
@@ -151,87 +153,46 @@ jobs:
|
||||
print("changed" if compat.update_readme(rows) else "unchanged")
|
||||
PY
|
||||
|
||||
if ! git diff --quiet -- README.md; then
|
||||
git config user.name "truecloud-patch bot"
|
||||
git config user.email "bot@onetick.ninja"
|
||||
git add README.md
|
||||
git commit -m "docs: refresh the TrueNAS compatibility matrix"
|
||||
git push origin HEAD:main
|
||||
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
|
||||
|
||||
# A broken SHIPPED release is an outage: users are on it right now.
|
||||
# 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
|
||||
|
||||
- name: file a bug report (GitHub)
|
||||
if: ${{ steps.report.outputs.broken == '1' && contains(github.server_url, 'github.com') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TITLE: "TrueNAS compatibility: the patch's assumptions no longer hold"
|
||||
run: |
|
||||
# One issue per set of broken refs, reopened/updated rather than duplicated
|
||||
# daily -- a bot that files the same issue every morning gets muted, and
|
||||
# then it is not a warning system any more.
|
||||
# Lowest-numbered match, for the same reason as the Gitea step below.
|
||||
existing="$(gh issue list --state all --search "$TITLE" \
|
||||
--json number,title \
|
||||
--jq '[.[] | select(.title == env.TITLE) | .number] | min // empty')"
|
||||
|
||||
if [ -n "$existing" ]; then
|
||||
gh issue comment "$existing" --body-file /tmp/issue.md
|
||||
gh issue reopen "$existing" 2>/dev/null || true
|
||||
else
|
||||
gh issue create --title "$TITLE" --body-file /tmp/issue.md
|
||||
fi
|
||||
|
||||
- name: file a bug report (Gitea)
|
||||
if: ${{ steps.report.outputs.broken == '1' && !contains(github.server_url, 'github.com') }}
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}
|
||||
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
TITLE: "TrueNAS compatibility: the patch's assumptions no longer hold"
|
||||
run: |
|
||||
# python3, not jq: jq is not guaranteed on a self-hosted runner, and a bug
|
||||
# report that dies on a missing tool is a warning system that does not warn.
|
||||
python3 - <<'PY'
|
||||
import json, os, urllib.error, urllib.request
|
||||
|
||||
api, token, title = os.environ["API"], os.environ["TOKEN"], os.environ["TITLE"]
|
||||
with open("/tmp/issue.md", encoding="utf-8") as fh:
|
||||
body = fh.read()
|
||||
headers = {"Authorization": f"token {token}",
|
||||
"Content-Type": "application/json"}
|
||||
|
||||
def call(url, method, data=None):
|
||||
req = urllib.request.Request(
|
||||
url, method=method, headers=headers,
|
||||
data=json.dumps(data).encode() if data else None)
|
||||
with urllib.request.urlopen(req) as r: # noqa: S310
|
||||
return json.load(r) if r.length != 0 else {}
|
||||
|
||||
# Same title => same issue. Comment on it rather than filing a new one every
|
||||
# morning: a bot that duplicates itself daily gets muted, and then it is not
|
||||
# a warning system any more.
|
||||
# LOWEST-numbered match, not "whichever the API returns first". Two issues
|
||||
# with the same title already existed once (the old title embedded the ref
|
||||
# list, so the identity changed when that set changed), and an
|
||||
# order-dependent pick would have alternated between them, reopening one and
|
||||
# commenting on the other. Lowest number is stable no matter what the API
|
||||
# sorts by.
|
||||
issues = call(f"{api}/issues?state=all&type=issues", "GET")
|
||||
matches = sorted((i for i in issues if i["title"] == title),
|
||||
key=lambda i: i["number"])
|
||||
match = matches[0] if matches else None
|
||||
|
||||
if match:
|
||||
n = match["number"]
|
||||
call(f"{api}/issues/{n}/comments", "POST", {"body": body})
|
||||
call(f"{api}/issues/{n}", "PATCH", {"state": "open"})
|
||||
print(f"commented on and reopened issue #{n}")
|
||||
else:
|
||||
made = call(f"{api}/issues", "POST", {"title": title, "body": body})
|
||||
print(f"filed issue #{made['number']}")
|
||||
PY
|
||||
|
||||
Reference in New Issue
Block a user