Repository navigation
Description
What's broken
Distinct roster members with an empty Apache ID cell overwrite each other's votes. Three valid approvals can be reduced to one and reported as FAILED. With a mixed roster, separate dissenting votes can also disappear and turn a tied list vote into PASSED.
Which layer
The deterministic voter-identity step in vote_tally/tally.py, confirmed on upstream main at 30ee18c2a7d46a42ce94278beb197fa106e94763.
parse_roster() accepts rows with a primary email and no Apache ID. resolve_binding() resolves their primary emails as binding. However, _identity() returns the same roster: key for all of them, so the latest-vote map merges different people.
How to reproduce
From that checkout, using Python 3.11+ and uv. All identities below are synthetic.
uv run --directory tools/release-management python - <<'PY'
from release_management.vote_tally import tally as t
header = "| Apache ID | Primary email |\n|---|---|\n"
names = ("alice", "bob", "carol")
blank = header + "".join(f"| | {n}@example.com |\n" for n in names)
filled = header + "".join(f"| {n} | {n}@example.com |\n" for n in names)
votes = [{"from": f"{n}@example.com", "value": "+1"} for n in names]
def show(name, roster, votes, mechanism, minimum=None):
out = t.tally(votes, t.parse_roster(roster), {}, False, mechanism, minimum)
print(name, out["binding_plus1"], out["binding_minus1"],
out["result"], out["proposed_label"])
print("superseded:", [(s["identity"], s["from"])
for s in out["superseded_votes"]])
show("blank IDs", blank, votes, "pr-approval", 2)
show("control", filled, votes, "pr-approval", 2)
opponents = ("erin", "frank", "grace")
mixed = filled + "".join(f"| | {n}@example.com |\n" for n in opponents)
dissent = [{"from": f"{n}@example.com", "value": "-1"} for n in opponents]
show("mixed tie", mixed, votes + dissent, "dev-list-vote")
PYActual summaries:
blank IDs 1 0 FAILED rc-rolled
control 3 0 PASSED vote-passed
mixed tie 3 1 PASSED vote-passed
The blank-ID case reports Alice and Bob as superseded under the identical key roster:. The mixed case reports Erin and Frank as superseded by other people.
Expected vs actual
The one-person-one-vote contract permits a later vote from the same person to supersede their earlier vote. Different roster members must remain distinct.
| Case | Expected | Actual |
|---|---|---|
| Three email-only approvers; minimum two | 3 approvals, PASSED, vote-passed | 1 approval, FAILED, rc-rolled |
| Same members with distinct IDs | 3 approvals, PASSED | 3 approvals, PASSED |
| Three members for and three against | 3:3, FAILED, rc-rolled | 3:1, PASSED, vote-passed |
Surface area (optional)
The shared identity step affects counts under all four mechanisms. Only dev-list-vote and configured pr-approval calculate a result in code; github-discussion and maintainer-roster retain null result and label values.
This is a counting defect in an already accepted roster shape. It does not require changing eligibility or voting thresholds. superseded_votes makes the anomaly visible to a careful RM, and the RM still confirms subsequent actions. This report does not establish an actual release incident.
The fix should retain nonempty Apache IDs as roster identity keys and use the existing normalized-address identity when the ID is absent. Audit handle resolution is outside this issue.
Environment (optional)
Upstream main: 30ee18c2a7d46a42ce94278beb197fa106e94763. Reproduced locally with Python 3.14 on Windows; regression and package checks also run with Python 3.12 on Linux.
AI disclosure and audit status
AI-assisted report prepared with Codex (GPT-6), using synthetic examples. Contributor review is complete; this fork issue is retained as the audit record.
Upstream submission
- Upstream bug report: vote-tally: roster members without Apache IDs overwrite each other's votes apache/magpie#1912
- Ready-for-review upstream PR: fix(release-vote-tally): keep email-only roster voters distinct apache/magpie#1913
- Reviewed audit commit:
1089c512a04af5786ea70ef22e813e9ab400854b. - Upstream commit:
32407c7087c614592a20a6e369361738fcc31c6a, based on6fafad25b1b1bc239c8863d09d757fa3cf71d72a. - The two changed files have identical Git blob hashes in the audit and upstream commits. This fork branch and Draft PR remain the audit snapshot.
Activity