merge: the report's body is truth; comments are the changelog
This commit is contained in:
@@ -48,6 +48,19 @@ worse than no alert, because one day it carries a security fix.
|
|||||||
count as shipped and a break in it would fail the build as a live outage — on a
|
count as shipped and a break in it would fail the build as a live outage — on a
|
||||||
version nobody is running yet.
|
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-dev` relabel 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\n` does not cause a rewrite
|
||||||
|
every run) and comments remain strictly a changelog of real changes.
|
||||||
|
|
||||||
- **The compatibility bot filed a new duplicate bug report on every Gitea run.**
|
- **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
|
`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
|
`pull_request` key. GitHub omits that key on a plain issue; Gitea sends it as
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ dangerous thing this file can say -- it means "TrueNAS does this now, retire the
|
|||||||
module" -- and it rests on nothing more than a substring match.
|
module" -- and it rests on nothing more than a substring match.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -725,3 +726,83 @@ class TestMasterIsNotTheNextRelease:
|
|||||||
# ...and the ordinary rows are untouched.
|
# ...and the ordinary rows are untouched.
|
||||||
assert "| 25.10.4 |" in md
|
assert "| 25.10.4 |" in md
|
||||||
assert "| 26.0.0-BETA.3 _(unreleased)_ |" in md
|
assert "| 26.0.0-BETA.3 _(unreleased)_ |" in md
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheBodyIsTruthAndCommentsAreTheChangelog:
|
||||||
|
"""An unchanged fingerprint used to freeze the BODY, not just silence the comments.
|
||||||
|
|
||||||
|
Two different questions were sharing one answer. "Have the findings changed?" gates
|
||||||
|
COMMENTS -- they notify, and a daily "still broken, same as yesterday" is what
|
||||||
|
teaches people to ignore the one that finally matters. But "is the body still
|
||||||
|
true?" gates the BODY, and editing a body notifies nobody, so keeping it honest
|
||||||
|
costs nothing.
|
||||||
|
|
||||||
|
Conflated, an unchanged fingerprint meant the report could never be corrected --
|
||||||
|
and the fingerprint deliberately ignores everything that moves on its own, which
|
||||||
|
includes how a row is LABELLED. Relabelling master `27-dev` would have reached the
|
||||||
|
README and never the issue anybody opens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ROWS = [{"ref": "master", "unreleased": True,
|
||||||
|
"modules": check_files(with_(**{
|
||||||
|
"plugins/cloud_backup/restic.py":
|
||||||
|
"class ResticConfig:\n cmd: list\n\n"
|
||||||
|
"def get_restic_config(entry, credentials):\n pass\n",
|
||||||
|
}))}]
|
||||||
|
|
||||||
|
def _run(self, monkeypatch, tmp_path, existing_body):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake(url, token, method="GET", data=None):
|
||||||
|
calls.append((method, url, data))
|
||||||
|
if url.endswith("/issues?state=all&per_page=100&limit=100"):
|
||||||
|
return [{"number": 1, "title": compat_publish.TITLE,
|
||||||
|
"state": "open", "body": existing_body,
|
||||||
|
"pull_request": None}]
|
||||||
|
return {"number": 1}
|
||||||
|
|
||||||
|
monkeypatch.setattr(compat_publish, "_call", fake)
|
||||||
|
matrix = tmp_path / "m.json"
|
||||||
|
matrix.write_text(json.dumps(self.ROWS))
|
||||||
|
compat_publish.main([
|
||||||
|
"prog", "--api", "https://forge/api", "--token", "t",
|
||||||
|
"--matrix", str(matrix)])
|
||||||
|
return calls
|
||||||
|
|
||||||
|
def _writes(self, calls):
|
||||||
|
patched = [c for c in calls if c[0] == "PATCH"]
|
||||||
|
commented = [c for c in calls if c[0] == "POST" and c[1].endswith("/comments")]
|
||||||
|
return patched, commented
|
||||||
|
|
||||||
|
def test_identical_body_and_findings_touches_nothing(self, monkeypatch, tmp_path):
|
||||||
|
body = compat.render_issue(self.ROWS)
|
||||||
|
patched, commented = self._writes(self._run(monkeypatch, tmp_path, body))
|
||||||
|
assert not patched and not commented, "a quiet run must be completely silent"
|
||||||
|
|
||||||
|
def test_a_relabel_refreshes_the_body_but_says_NOTHING(self, monkeypatch, tmp_path):
|
||||||
|
# Same findings (same fingerprint), different rendering -- the exact shape of
|
||||||
|
# the master -> 27-dev relabel.
|
||||||
|
stale = compat.render_issue(self.ROWS).replace("27-dev", "unreleased")
|
||||||
|
assert compat.extract_fingerprint(stale) == compat.fingerprint(self.ROWS)
|
||||||
|
|
||||||
|
patched, commented = self._writes(self._run(monkeypatch, tmp_path, stale))
|
||||||
|
assert patched, "the body was left stale, so the issue keeps telling lies"
|
||||||
|
assert "27-dev" in patched[0][2]["body"]
|
||||||
|
assert not commented, (
|
||||||
|
"a rendering change is not news -- commenting on it is how the bot gets "
|
||||||
|
"muted before the next real finding"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_REAL_findings_change_still_comments(self, monkeypatch, tmp_path):
|
||||||
|
# ...and the fix must not have made it mute.
|
||||||
|
stale = compat.render_issue(self.ROWS).replace(
|
||||||
|
compat.fingerprint(self.ROWS), "0" * 16)
|
||||||
|
patched, commented = self._writes(self._run(monkeypatch, tmp_path, stale))
|
||||||
|
assert patched and commented, "a genuine change must still notify"
|
||||||
|
|
||||||
|
def test_a_body_differing_only_by_CRLF_is_not_rewritten(self, monkeypatch, tmp_path):
|
||||||
|
# Forges round-trip line endings. Without normalising, every run would rewrite
|
||||||
|
# the body -- silent, but it churns updated_at and looks freshly touched daily.
|
||||||
|
body = compat.render_issue(self.ROWS).replace("\n", "\r\n")
|
||||||
|
patched, _ = self._writes(self._run(monkeypatch, tmp_path, body))
|
||||||
|
assert not patched
|
||||||
|
|||||||
+35
-5
@@ -62,6 +62,19 @@ def _call(url, token, method="GET", data=None):
|
|||||||
return json.load(r) if r.length != 0 else {}
|
return json.load(r) if r.length != 0 else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _same(a, b):
|
||||||
|
"""Is the issue body already what we would write?
|
||||||
|
|
||||||
|
Compared after normalising line endings and trailing space: forges are free to
|
||||||
|
round-trip `\r\n`, and a body that only "differs" by that would be rewritten on
|
||||||
|
every single run -- a silent edit, but a pointless one that churns `updated_at`
|
||||||
|
and makes the issue look freshly touched every morning.
|
||||||
|
"""
|
||||||
|
def norm(s):
|
||||||
|
return "\n".join(line.rstrip() for line in (s or "").replace("\r\n", "\n").split("\n")).strip()
|
||||||
|
return norm(a) == norm(b)
|
||||||
|
|
||||||
|
|
||||||
def find_issue(api, token, title):
|
def find_issue(api, token, title):
|
||||||
"""The LOWEST-numbered issue with this title, open or closed.
|
"""The LOWEST-numbered issue with this title, open or closed.
|
||||||
|
|
||||||
@@ -124,11 +137,24 @@ def main(argv):
|
|||||||
have = extract_fingerprint(issue.get("body") or "")
|
have = extract_fingerprint(issue.get("body") or "")
|
||||||
n = issue["number"]
|
n = issue["number"]
|
||||||
|
|
||||||
# ── the findings are UNCHANGED: say nothing ──────────────────────────────
|
# ── two different questions, and they were being answered with one answer ────
|
||||||
#
|
#
|
||||||
# This is the whole point. A daily "still broken, same as yesterday" comment is
|
# * IS THE BODY STILL TRUE? -> if not, rewrite it. Editing an issue body
|
||||||
# what taught everyone to ignore the last one.
|
# notifies NOBODY on either forge, so keeping it honest is free.
|
||||||
if have == want and issue["state"] == "open":
|
# * HAVE THE FINDINGS CHANGED? -> only then comment. Comments DO notify, and a
|
||||||
|
# daily "still broken, same as yesterday" is what teaches everyone to ignore
|
||||||
|
# the one that finally matters.
|
||||||
|
#
|
||||||
|
# Conflating them meant an unchanged FINGERPRINT froze the BODY. The fingerprint
|
||||||
|
# deliberately ignores everything that moves on its own -- healthy rows, the
|
||||||
|
# hardware-verified column, point releases, how a row is LABELLED -- so none of
|
||||||
|
# that could ever reach the report. Relabelling master `27-dev` (it is not the
|
||||||
|
# next release; a red row there was reading as "the version you are about to
|
||||||
|
# install is broken") would have shipped to the README and never to the issue
|
||||||
|
# anybody actually opens.
|
||||||
|
body_is_current = _same(issue.get("body"), body)
|
||||||
|
|
||||||
|
if have == want and issue["state"] == "open" and body_is_current:
|
||||||
print(f"#{n} is already current ({want}) — staying quiet")
|
print(f"#{n} is already current ({want}) — staying quiet")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -146,8 +172,12 @@ def main(argv):
|
|||||||
)
|
)
|
||||||
_call(f"{args.api}/issues/{n}/comments", args.token, "POST", {"body": note})
|
_call(f"{args.api}/issues/{n}/comments", args.token, "POST", {"body": note})
|
||||||
print(f"updated #{n}: {have} -> {want}")
|
print(f"updated #{n}: {have} -> {want}")
|
||||||
else:
|
elif issue["state"] != "open":
|
||||||
print(f"reopened #{n}")
|
print(f"reopened #{n}")
|
||||||
|
else:
|
||||||
|
# Same findings, new rendering. Silent by design: nothing has changed that
|
||||||
|
# anybody needs waking up for, but the report should not be telling lies.
|
||||||
|
print(f"#{n}: findings unchanged ({want}); body refreshed silently")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user