parity-main: a failure that passes again at HEAD counts as a pass

Twice today (runs 40301239508, 40311712302) mob_ai failed in parity-main, passed when bisect ran it again at
the same HEAD, and the run still ended red, so no game release could follow main for three hours. The
harness step now continues on error and the bisect step gives the verdict: 0 when every failed harness
passes again at HEAD, 1 when they fail at the base too or a merge is named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-26 14:12:04 +01:00
parent 8087d8bf58
commit 9e76f8c4f0
3 changed files with 18 additions and 13 deletions
+6 -3
View File
@@ -48,6 +48,9 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: echo "sha=$(python3 tools/parity_main.py base)" >> "$GITHUB_OUTPUT"
- name: The harnesses the merges since then can affect
id: harnesses
# A failure here is judged by the next step: the run passes when every failed harness passes again at HEAD.
continue-on-error: true
env:
PARITY_HOSTS: ''
BASE_SHA: ${{ steps.base.outputs.sha }}
@@ -55,8 +58,8 @@ jobs:
echo "Merges since ${BASE_SHA}:"
git log --first-parent --format=' %h %s' "$BASE_SHA..HEAD"
python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400
- name: The merge that broke them
if: failure()
- name: A flake, or the merge that broke them
if: steps.harnesses.outcome == 'failure'
env:
GH_TOKEN: ${{ github.token }}
PARITY_HOSTS: ''
@@ -65,7 +68,7 @@ jobs:
rm -rf .local/parity-main-logs && cp -R .local/parity-runner/logs .local/parity-main-logs 2>/dev/null || true
python3 tools/parity_main.py bisect "$BASE_SHA" --comment
- name: Logs of the failed harnesses
if: failure()
if: steps.harnesses.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: parity-main-logs
+3 -2
View File
@@ -45,7 +45,7 @@ class ParityMainTests(unittest.TestCase):
last_run.parent.mkdir(parents=True)
last_run.write_text('{"failed": ["music"]}')
real_run = parity_main.subprocess.run
for broken_from, blamed in ((None, None), (0, None), (2, "2")):
for broken_from, blamed, code in ((None, None, 0), (0, None, 1), (2, "2", 1)):
comments = []
# git runs for real in the scratch repo; a `gh pr comment` is only recorded, never sent.
fake_run = lambda command, **kwargs: (comments.append(command) if command[0] == "gh"
@@ -55,7 +55,8 @@ class ParityMainTests(unittest.TestCase):
and commits.index(git("rev-parse", "HEAD")) >= broken_from), \
mock.patch.object(parity_main.subprocess, "run", fake_run), \
mock.patch("builtins.print") as printed:
parity_main.bisect(commits[0], 60, comment=True)
self.assertEqual(parity_main.bisect(commits[0], 60, comment=True), code,
"0 only when the failures pass again at HEAD")
if blamed:
self.assertEqual([command[3] for command in comments], [blamed])
else:
+9 -8
View File
@@ -78,34 +78,35 @@ def bisect(since, timeout, comment):
failed = json.loads(LAST_RUN.read_text(encoding="utf-8")).get("failed") or []
if not failed:
print("No failed harness recorded: nothing to bisect.")
return 0
return 1
head = git("rev-parse", "HEAD").stdout.strip()
commits = git("rev-list", "--first-parent", "--reverse", f"{since}..{head}").stdout.split()
if not commits:
print("No merge since the base.")
return 0
return 1
def fails(commit):
print(f"--- {commit[:9]} {git('log', '-1', '--format=%s', commit).stdout.strip()}", flush=True)
git("checkout", "-q", "--detach", commit)
return run_harnesses(failed, timeout)
def blame_nobody(why):
def blame_nobody(why, code):
line = f"parity-main: {', '.join(failed)} {why}: no merge is named."
print(line)
if os.environ.get("GITHUB_STEP_SUMMARY"):
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
summary.write(f"### {line}\n")
return 0
return code
# A merge is named only when the harnesses fail again at HEAD and pass at the base: otherwise the search would
# end on the newest merge (a flake, or a runner missing files, passes everywhere now) or the oldest one (the
# runner fails at every commit) and blame it for nothing.
# runner fails at every commit) and blame it for nothing. The exit code is the run's verdict: 0 only when every
# failed harness passes when run again at HEAD (the commit is good, so releases may follow it), 1 otherwise.
try:
if not fails(head):
return blame_nobody(f"pass when run again at {head[:9]}: a flake or the runner")
return blame_nobody(f"pass when run again at {head[:9]}: a flake, counted as a pass", 0)
if fails(since):
return blame_nobody(f"fail at the base {since[:9]} too: the runner or a flake")
return blame_nobody(f"fail at the base {since[:9]} too: the runner or a flake", 1)
culprit = first_bad(commits, fails) if len(commits) > 1 else commits[0]
finally:
git("checkout", "-q", "--detach", head)
@@ -122,7 +123,7 @@ def bisect(since, timeout, comment):
body = (f"🤖 Agent: parity-main found that this merge breaks {', '.join(f'`{name}`' for name in failed)} on main "
f"(bisected over {len(commits)} merges: {url}). Releases wait until parity-main passes again.")
subprocess.run(["gh", "pr", "comment", number.group(1), "--repo", REPO, "--body", body])
return 0
return 1
def main():