Repository navigation
fix: list every team member and match names exactly [PLAT-7673] - #734
Conversation
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>
There was a problem hiding this comment.
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, whenoffset >= totalSize, or, if there is nototalSize, on a short page. A server that returns fewer rows thanmaxdoes not end the walk early. ThePaginatorTestcases cover these paths, including stopping on a full last page and stopping as soon as a match is found.- Participants.
findWorkspaceParticipantused to return the first participant of the requested type, whatever its name. That meantparticipants delete/update, andadd --overwrite, could act on the wrong participant. The updatedtestAddParticipantWithOverwritefixture (it now deletes participant36791779798370instead 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,AddCmdfalls back tosetUserNameOrEmail(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 memberslisting. The old fixedmax=100and its TODO are replaced byfetchAll. 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 insrc/main. Its only use was the false "multiple members" failure that the newmembers deletetest reproduces. - JSON output.
totalSizeonMembersList,ParticipantsListandTeamsListis a new field, so existing consumers are unaffected. TheMembersListconstructor change has a single caller, which the PR updates.
Non-blocking notes
Paginatorhas no limit on the number of pages. If a server reported nototalSizeand also ignoredoffset, sending full pages every time, the walk would never end. The Platform endpoints in this PR do reporttotalSize, 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: thetotalSizein 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 sharedisUserhelper 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
left a comment
There was a problem hiding this comment.
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!
Fixes PLAT-7673.
Scripts that reconcile users against
twoutput get wrong answers today.tw teams membersdrops everyone past the 100th member without a warning, and the name lookups behind theparticipantsandmemberscommands act on the first substring match, which can be a different user.What was broken
tw teams membersstops at 100. It hardcodedmax=100, offset=0(next to a//TODO use pagination) and printed the first page as if it were the whole team.findOrganizationMemberByName,findOrganizationCollaboratorByNameandfindWorkspaceParticipantused the first result without checking the name. In a workspace withaidananddan,tw participants delete -n dan -t MEMBERdeleted aidan.update,addandadd --overwriteresolve names the same way, and-n dan@seqera.ioresolves toaidan@seqera.io.tw members update/delete -u danrefused to run whenever another user's name or email containeddan("Multiple members were found").devwhen 55 other teams contain "dev".totalSizein JSON.members list,teams listandparticipants listaccept--max/--offset, but a script couldn't tell whether a page was the last. Withparticipants list --type, a short page isn't even a reliable signal, because--typefilters 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:
fix(teams): list every member of a team: a newutils/Paginatorfetches 100 rows at a time until it reachestotalSize(it truststotalSizeover a short page).tw teams membersuses it.feat: report totalSize in paginated list JSON:totalSizeis added to the JSON ofmembers list,teams listandparticipants list, andmembers listprints the page footer the other two already show. Forparticipants list --type,totalSizecounts every type.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 coversparticipants add/update/delete,--overwrite,members update/delete,teams members deleteand the team lookups.MembersMultiplicityExceptionhad no other use and is removed.Behaviour changes
-n "Dan Smith",-u da) no longer resolves to whoever matched first.participants addfalls back to adding by user name or email, as it already did when nothing matched. The other commands report not found.members update/deleteno longer fail with "Multiple members were found".totalSizefield.Merged with #702
#702 merged while this PR was open, and master is merged into this branch (c89fd62).
findWorkspaceParticipantis 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).participants addis kept and works with the exact match: a name that isn't exactly an org member now reaches the service-account lookup.ParticipantsCmdTestcases from both PRs are kept. feat: add the agents and service-accounts command groups [NOTASK] #702 had made the same correction totestAddParticipantWithOverwrite.Tests
./gradlew teston the merged branch: 964 tests, 0 failures. Each of the three commits also passes the affected test classes on its own.PaginatorTest(6 cases),TeamMembersCmdTest.testListPagesThroughEveryMember(101 members over two pages), and "dan vs aidan" cases forparticipants delete,participants addandmembers delete.ParticipantsCmdTest.testAddParticipantWithOverwriteasserted the bug: the overwrite ofjuliodeletedjfernandez74's participant. It now expects julio's.Manual QA against a local Platform
Local livedev backend (API 1.261.0), with
twbuilt 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 ofdev, and usersdan,aidananda-dan-01toa-dan-26.teams members -t big-teamteams members -t devteams members -t dan-team delete -m danparticipants add -n dan -t MEMBERparticipants add -n DAN@SEQERA.IO -t MEMBERparticipants delete -n dan -t MEMBERparticipants update -n dan -t MEMBER -r VIEWparticipants add --overwrite -n dan -t MEMBERparticipants add -n dev -t TEAMmembers update -u dan -r ownermembers delete -u dan--output=json members list / teams list / participants listtotalSizetotalSize130 / 59 / 4members list --page 2 --max 5Not 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.firstMemberhas the same first-match pattern, so--allow-user dancould allowaidan.LabelsFinderstops after the first page when 101 to 199 labels match.overwrite.pylists members, teams and participants without--max, so it only sees the first 100 of each.🤖 Generated with Claude Code