Skip to content

fix: list every team member and match names exactly [PLAT-7673] - #734

Merged
joaquimgamero merged 4 commits into
masterfrom
fix/PLAT-7673-paginate-team-members-and-exact-lookups
Nov 10, 2026
Merged

joaquimgamero merged 4 commits into
masterfrom
fix/PLAT-7673-paginate-team-members-and-exact-lookups

Conversation

@joaquimgamero

@joaquimgamero joaquimgamero commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Fixes PLAT-7673.

Scripts that reconcile users against tw output get wrong answers today. tw teams members drops everyone past the 100th member without a warning, and the name lookups behind the participants and members commands act on the first substring match, which can be a different user.

What was broken

  • tw teams members stops at 100. It hardcoded max=100, offset=0 (next to a //TODO use pagination) and printed the first page as if it were the whole team.
  • Name lookups take the first fuzzy match. The list endpoints search usernames, emails and names by substring, sorted by username. findOrganizationMemberByName, findOrganizationCollaboratorByName and findWorkspaceParticipant used the first result without checking the name. In a workspace with aidan and dan, tw participants delete -n dan -t MEMBER deleted aidan. update, add and add --overwrite resolve names the same way, and -n dan@seqera.io resolves to aidan@seqera.io.
  • tw members update/delete -u dan refused to run whenever another user's name or email contained dan ("Multiple members were found").
  • Lookups only read one page. Team-by-name read 50 search results and team-member-by-username read 25, so the exact match could be missed, for example a team named dev when 55 other teams contain "dev".
  • No totalSize in JSON. members list, teams list and participants list accept --max/--offset, but a script couldn't tell whether a page was the last. With participants list --type, a short page isn't even a reliable signal, because --type filters after the fetch.

The team lookup that only saw the first 50 teams, reported alongside this, was already fixed in v0.42.0 by 543047c.

Fix

One commit per change:

  1. fix(teams): list every member of a team: a new utils/Paginator fetches 100 rows at a time until it reaches totalSize (it trusts totalSize over a short page). tw teams members uses it.
  2. feat: report totalSize in paginated list JSON: totalSize is added to the JSON of members list, teams list and participants list, and members list prints the page footer the other two already show. For participants list --type, totalSize counts every type.
  3. fix(participants): act only on an exact name match: every lookup by name pages through the search results and keeps only an exact username or email (case-insensitive) or team name. That covers participants add/update/delete, --overwrite, members update/delete, teams members delete and the team lookups. MembersMultiplicityException had no other use and is removed.

Behaviour changes

  • A partial name (-n "Dan Smith", -u da) no longer resolves to whoever matched first. participants add falls back to adding by user name or email, as it already did when nothing matched. The other commands report not found.
  • members update/delete no longer fail with "Multiple members were found".
  • The JSON output of the three list commands gains a totalSize field.

Merged with #702

#702 merged while this PR was open, and master is merged into this branch (c89fd62).

  • Its exact-match filter in findWorkspaceParticipant is replaced by this PR's version, which matches the same fields but also pages through the search and ignores case on usernames (Platform usernames are lowercase-only).
  • Its service-account fallback in participants add is kept and works with the exact match: a name that isn't exactly an org member now reaches the service-account lookup.
  • The new ParticipantsCmdTest cases from both PRs are kept. feat: add the agents and service-accounts command groups [NOTASK] #702 had made the same correction to testAddParticipantWithOverwrite.

Tests

  • ./gradlew test on the merged branch: 964 tests, 0 failures. Each of the three commits also passes the affected test classes on its own.
  • New: PaginatorTest (6 cases), TeamMembersCmdTest.testListPagesThroughEveryMember (101 members over two pages), and "dan vs aidan" cases for participants delete, participants add and members delete.
  • ParticipantsCmdTest.testAddParticipantWithOverwrite asserted the bug: the overwrite of julio deleted jfernandez74's participant. It now expects julio's.

Manual QA against a local Platform

Local livedev backend (API 1.261.0), with tw built from master and from this branch. State was checked through the REST API after every command and reset between builds. Seed: a 101-member team, 55 teams containing "dev" that sort ahead of dev, and users dan, aidan and a-dan-01 to a-dan-26.

Command master this branch
teams members -t big-team 100 of 101 101
teams members -t dev Team 'dev' not found found
teams members -t dan-team delete -m dan Member 'dan' not found dan removed
participants add -n dan -t MEMBER added a-dan-01 added dan
participants add -n DAN@SEQERA.IO -t MEMBER added aidan added dan
participants delete -n dan -t MEMBER deleted aidan deleted dan
participants update -n dan -t MEMBER -r VIEW changed aidan changed dan
participants add --overwrite -n dan -t MEMBER deleted aidan, added a-dan-01 replaced dan
participants add -n dev -t TEAM Team 'dev' not found added
members update -u dan -r owner Multiple members were found dan updated
members delete -u dan Multiple members were found dan removed
--output=json members list / teams list / participants list no totalSize totalSize 130 / 59 / 4
members list --page 2 --max 5 no footer "…from a total of 130 entries"
missing team or participant not found same error

Not covered: the native binary (the three list responses are registered with allDeclaredFields, so the new field should serialize there too) and collaborator lookups beyond the unit tests.

Not in this PR

  • AbstractStudiosCmd.firstMember has the same first-match pattern, so --allow-user dan could allow aidan.
  • LabelsFinder stops after the first page when 101 to 199 labels match.
  • seqerakit's overwrite.py lists members, teams and participants without --max, so it only sees the first 100 of each.

🤖 Generated with Claude Code

joaquimgamero and others added 3 commits November 4, 2026 16:25
tw teams members asked for the first 100 members and printed them with
no warning, so a larger team came back silently truncated. Fetch every
page until totalSize is reached, through a shared Paginator that the
name lookups reuse. It trusts totalSize over a short page, so a server
that serves smaller pages can't end the walk early.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
members list, teams list and participants list accept --max and
--offset, but their JSON never said how many rows exist, so a script
could only spot the last page by getting a short one. That guess fails
for participants list --type, which filters each page after fetching
it. members list also gains the page footer the other two print.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lookups behind participants add, update and delete, members update
and delete, and teams members delete searched by substring over
usernames, emails and names, then used the first result. So
'tw participants delete -n dan' deleted 'aidan', and adding
'dan@seqera.io' added 'aidan@seqera.io'. members update and delete
refused instead, with a multiple-members error.

Page through the search results and keep only an exact username or
email (case-insensitive) or team name. Team lookups page too, since
more than 50 teams can contain the name. testAddParticipantWithOverwrite
asserted the old behaviour: the overwrite of 'julio' deleted
jfernandez74's participant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joaquimgamero
joaquimgamero marked this pull request as ready for review November 4, 2026 15:40

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. This fixes one problem everywhere it appears: name lookups used to trust the substring search on the list endpoints and read only its first page. Every lookup now pages through the results and keeps only an exact match.

What I checked

  • Paginator.findFirst / fetchAll. The loop stops when a page is empty or null, when offset >= totalSize, or, if there is no totalSize, on a short page. A server that returns fewer rows than max does not end the walk early. The PaginatorTest cases cover these paths, including stopping on a full last page and stopping as soon as a match is found.
  • Participants. findWorkspaceParticipant used to return the first participant of the requested type, whatever its name. That meant participants delete/update, and add --overwrite, could act on the wrong participant. The updated testAddParticipantWithOverwrite fixture (it now deletes participant 36791779798370 instead of the first one listed) shows this was a real bug, and the new predicate fixes it. Team names are compared exactly, and usernames and emails without regard to case, which matches the member lookups.
  • participants add. If there is no exact member or collaborator match, AddCmd falls back to setUserNameOrEmail(name) as before. A name that only partly matches now goes through that fallback instead of silently choosing whoever the search returned first. That is the intended behaviour.
  • teams members listing. The old fixed max=100 and its TODO are replaced by fetchAll. Dropping the "has no members" error when the response is null is fine; an empty list is the better result.
  • Removed MembersMultiplicityException. It has no callers left in src/main. Its only use was the false "multiple members" failure that the new members delete test reproduces.
  • JSON output. totalSize on MembersList, ParticipantsList and TeamsList is a new field, so existing consumers are unaffected. The MembersList constructor change has a single caller, which the PR updates.

Non-blocking notes

  • Paginator has no limit on the number of pages. If a server reported no totalSize and also ignored offset, sending full pages every time, the walk would never end. The Platform endpoints in this PR do report totalSize, so this is theoretical. A simple guard against an offset that doesn't advance or a hard maximum page count would rule it out cheaply.
  • participants list --type: the totalSize in the footer and JSON counts participants of every type, not only the filtered ones. The code comment says so, but users may not expect it. Consider a short note in the command help.
  • The exact-match comparison is case-insensitive for members and participants but case-sensitive for team members in AbstractTeamsCmd.findMemberByUsername (unchanged from before). It's minor, but a shared isUser helper would make them consistent.

I did not build the project or run the tests in this checkout. This review is based on reading the diff and the surrounding code.

#702 added an exact-match filter to findWorkspaceParticipant. Keep this
branch's version, which pages through the search and matches the same
fields (usernames case-insensitively). Keep both sides' new
ParticipantsCmdTest cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@educalleja educalleja left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM @joaquimgamero ! Tested all the use cases here and they work as intended.

One observation I noticed: We are adding the totalSize attribute in json responses to member list, teams list and participants list. That was the scope of PLAT-7673. There are many other operaitons that list items that don't have totalSize. To avoid scope creep, lets finish this PR as is and I will add a new task to address the rest of the list commands for consistency.

Besides that, good work man, it works like a charm!

@joaquimgamero
joaquimgamero merged commit 560e918 into master Nov 10, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants