Skip to content

vote-tally: roster members without Apache IDs overwrite each other's votes #27

Open

Description

@MakerFlyFly

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")
PY

Actual 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions