Skip to content

Commit eb878e5

Browse files
Copilotkarpikpl
andauthored
Fix duplicate users in GitHub Enterprise with multiple organizations (#227)
* Initial plan * Initial analysis and plan for deduplicating seats in enterprise Co-authored-by: karpikpl <[email protected]> * Implement seat deduplication for GitHub Enterprise with multiple organizations Co-authored-by: karpikpl <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: karpikpl <[email protected]>
1 parent 1f2ba12 commit eb878e5

File tree

5 files changed

+586
-3
lines changed

5 files changed

+586
-3
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"total_seats": 3,
3+
"seats": [
4+
{
5+
"created_at": "2021-08-03T18:00:00-06:00",
6+
"updated_at": "2021-09-23T15:00:00-06:00",
7+
"pending_cancellation_date": null,
8+
"last_activity_at": "2021-10-14T00:53:32-06:00",
9+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
10+
"assignee": {
11+
"login": "octocat_byEnterprise",
12+
"id": 1,
13+
"node_id": "MDQ6VXNlcjE=",
14+
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
15+
"gravatar_id": "",
16+
"url": "https://api.github.com/users/octocat",
17+
"html_url": "https://github.com/octocat",
18+
"followers_url": "https://api.github.com/users/octocat/followers",
19+
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
20+
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
21+
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
22+
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
23+
"organizations_url": "https://api.github.com/users/octocat/orgs",
24+
"repos_url": "https://api.github.com/users/octocat/repos",
25+
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
26+
"received_events_url": "https://api.github.com/users/octocat/received_events",
27+
"type": "User",
28+
"site_admin": false
29+
},
30+
"assigning_team": {
31+
"id": 1,
32+
"node_id": "MDQ6VGVhbTE=",
33+
"url": "https://api.github.com/teams/1",
34+
"html_url": "https://github.com/orgs/github/teams/justice-league",
35+
"name": "Justice League",
36+
"slug": "justice-league",
37+
"description": "A great team.",
38+
"privacy": "closed",
39+
"notification_setting": "notifications_enabled",
40+
"permission": "admin",
41+
"members_url": "https://api.github.com/teams/1/members{/member}",
42+
"repositories_url": "https://api.github.com/teams/1/repos",
43+
"parent": null
44+
}
45+
},
46+
{
47+
"created_at": "2021-09-23T18:00:00-06:00",
48+
"updated_at": "2021-09-23T15:00:00-06:00",
49+
"pending_cancellation_date": "2021-11-01",
50+
"last_activity_at": "2021-10-13T00:53:32-06:00",
51+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
52+
"assignee": {
53+
"login": "octokitten",
54+
"id": 1,
55+
"node_id": "MDQ76VNlcjE=",
56+
"avatar_url": "https://github.com/images/error/octokitten_happy.gif",
57+
"gravatar_id": "",
58+
"url": "https://api.github.com/users/octokitten",
59+
"html_url": "https://github.com/octokitten",
60+
"followers_url": "https://api.github.com/users/octokitten/followers",
61+
"following_url": "https://api.github.com/users/octokitten/following{/other_user}",
62+
"gists_url": "https://api.github.com/users/octokitten/gists{/gist_id}",
63+
"starred_url": "https://api.github.com/users/octokitten/starred{/owner}{/repo}",
64+
"subscriptions_url": "https://api.github.com/users/octokitten/subscriptions",
65+
"organizations_url": "https://api.github.com/users/octokitten/orgs",
66+
"repos_url": "https://api.github.com/users/octokitten/repos",
67+
"events_url": "https://api.github.com/users/octokitten/events{/privacy}",
68+
"received_events_url": "https://api.github.com/users/octokitten/received_events",
69+
"type": "User",
70+
"site_admin": false
71+
}
72+
},
73+
{
74+
"created_at": "2021-09-23T18:00:00-06:00",
75+
"updated_at": "2021-09-23T15:00:00-06:00",
76+
"pending_cancellation_date": "2021-11-01",
77+
"last_activity_at": "2021-10-12T00:53:32-06:00",
78+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
79+
"assignee": null,
80+
"assigning_team": null
81+
}
82+
]
83+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
{
2+
"total_seats": 2,
3+
"seats": [
4+
{
5+
"created_at": "2021-08-03T18:00:00-06:00",
6+
"updated_at": "2021-09-23T15:00:00-06:00",
7+
"pending_cancellation_date": null,
8+
"last_activity_at": "2021-10-14T00:53:32-06:00",
9+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
10+
"assignee": {
11+
"login": "octocat",
12+
"id": 1,
13+
"node_id": "MDQ6VXNlcjE=",
14+
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
15+
"gravatar_id": "",
16+
"url": "https://api.github.com/users/octocat",
17+
"html_url": "https://github.com/octocat",
18+
"followers_url": "https://api.github.com/users/octocat/followers",
19+
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
20+
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
21+
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
22+
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
23+
"organizations_url": "https://api.github.com/users/octocat/orgs",
24+
"repos_url": "https://api.github.com/users/octocat/repos",
25+
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
26+
"received_events_url": "https://api.github.com/users/octocat/received_events",
27+
"type": "User",
28+
"site_admin": false
29+
},
30+
"assigning_team": {
31+
"id": 1,
32+
"node_id": "MDQ6VGVhbTE=",
33+
"url": "https://api.github.com/teams/1",
34+
"html_url": "https://github.com/orgs/github/teams/justice-league",
35+
"name": "Team Alpha",
36+
"slug": "team-alpha",
37+
"description": "A great team.",
38+
"privacy": "closed",
39+
"notification_setting": "notifications_enabled",
40+
"permission": "admin",
41+
"members_url": "https://api.github.com/teams/1/members{/member}",
42+
"repositories_url": "https://api.github.com/teams/1/repos",
43+
"parent": null
44+
}
45+
},
46+
{
47+
"created_at": "2021-08-03T18:00:00-06:00",
48+
"updated_at": "2021-09-23T15:00:00-06:00",
49+
"pending_cancellation_date": null,
50+
"last_activity_at": "2021-10-16T00:53:32-06:00",
51+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
52+
"assignee": {
53+
"login": "octocat",
54+
"id": 1,
55+
"node_id": "MDQ6VXNlcjE=",
56+
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
57+
"gravatar_id": "",
58+
"url": "https://api.github.com/users/octocat",
59+
"html_url": "https://github.com/octocat",
60+
"followers_url": "https://api.github.com/users/octocat/followers",
61+
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
62+
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
63+
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
64+
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
65+
"organizations_url": "https://api.github.com/users/octocat/orgs",
66+
"repos_url": "https://api.github.com/users/octocat/repos",
67+
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
68+
"received_events_url": "https://api.github.com/users/octocat/received_events",
69+
"type": "User",
70+
"site_admin": false
71+
},
72+
"assigning_team": {
73+
"id": 2,
74+
"node_id": "MDQ6VGVhbTI=",
75+
"url": "https://api.github.com/teams/2",
76+
"html_url": "https://github.com/orgs/github/teams/beta-team",
77+
"name": "Team Beta",
78+
"slug": "team-beta",
79+
"description": "Another great team.",
80+
"privacy": "closed",
81+
"notification_setting": "notifications_enabled",
82+
"permission": "admin",
83+
"members_url": "https://api.github.com/teams/2/members{/member}",
84+
"repositories_url": "https://api.github.com/teams/2/repos",
85+
"parent": null
86+
}
87+
},
88+
{
89+
"created_at": "2021-08-04T18:00:00-06:00",
90+
"updated_at": "2021-09-24T15:00:00-06:00",
91+
"pending_cancellation_date": null,
92+
"last_activity_at": "2021-10-13T00:53:32-06:00",
93+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
94+
"assignee": {
95+
"login": "octokitten",
96+
"id": 2,
97+
"node_id": "MDQ76VNlcjI=",
98+
"avatar_url": "https://github.com/images/error/octokitten_happy.gif",
99+
"gravatar_id": "",
100+
"url": "https://api.github.com/users/octokitten",
101+
"html_url": "https://github.com/octokitten",
102+
"followers_url": "https://api.github.com/users/octokitten/followers",
103+
"following_url": "https://api.github.com/users/octokitten/following{/other_user}",
104+
"gists_url": "https://api.github.com/users/octokitten/gists{/gist_id}",
105+
"starred_url": "https://api.github.com/users/octokitten/starred{/owner}{/repo}",
106+
"subscriptions_url": "https://api.github.com/users/octokitten/subscriptions",
107+
"organizations_url": "https://api.github.com/users/octokitten/orgs",
108+
"repos_url": "https://api.github.com/users/octokitten/repos",
109+
"events_url": "https://api.github.com/users/octokitten/events{/privacy}",
110+
"received_events_url": "https://api.github.com/users/octokitten/received_events",
111+
"type": "User",
112+
"site_admin": false
113+
},
114+
"assigning_team": {
115+
"id": 1,
116+
"node_id": "MDQ6VGVhbTE=",
117+
"url": "https://api.github.com/teams/1",
118+
"html_url": "https://github.com/orgs/github/teams/justice-league",
119+
"name": "Team Alpha",
120+
"slug": "team-alpha",
121+
"description": "A great team.",
122+
"privacy": "closed",
123+
"notification_setting": "notifications_enabled",
124+
"permission": "admin",
125+
"members_url": "https://api.github.com/teams/1/members{/member}",
126+
"repositories_url": "https://api.github.com/teams/1/repos",
127+
"parent": null
128+
}
129+
},
130+
{
131+
"created_at": "2021-08-04T18:00:00-06:00",
132+
"updated_at": "2021-09-24T15:00:00-06:00",
133+
"pending_cancellation_date": null,
134+
"last_activity_at": "2021-10-15T00:53:32-06:00",
135+
"last_activity_editor": "vscode/1.77.3/copilot/1.86.82",
136+
"assignee": {
137+
"login": "octokitten",
138+
"id": 2,
139+
"node_id": "MDQ76VNlcjI=",
140+
"avatar_url": "https://github.com/images/error/octokitten_happy.gif",
141+
"gravatar_id": "",
142+
"url": "https://api.github.com/users/octokitten",
143+
"html_url": "https://github.com/octokitten",
144+
"followers_url": "https://api.github.com/users/octokitten/followers",
145+
"following_url": "https://api.github.com/users/octokitten/following{/other_user}",
146+
"gists_url": "https://api.github.com/users/octokitten/gists{/gist_id}",
147+
"starred_url": "https://api.github.com/users/octokitten/starred{/owner}{/repo}",
148+
"subscriptions_url": "https://api.github.com/users/octokitten/subscriptions",
149+
"organizations_url": "https://api.github.com/users/octokitten/orgs",
150+
"repos_url": "https://api.github.com/users/octokitten/repos",
151+
"events_url": "https://api.github.com/users/octokitten/events{/privacy}",
152+
"received_events_url": "https://api.github.com/users/octokitten/received_events",
153+
"type": "User",
154+
"site_admin": false
155+
},
156+
"assigning_team": {
157+
"id": 2,
158+
"node_id": "MDQ6VGVhbTI=",
159+
"url": "https://api.github.com/teams/2",
160+
"html_url": "https://github.com/orgs/github/teams/beta-team",
161+
"name": "Team Beta",
162+
"slug": "team-beta",
163+
"description": "Another great team.",
164+
"privacy": "closed",
165+
"notification_setting": "notifications_enabled",
166+
"permission": "admin",
167+
"members_url": "https://api.github.com/teams/2/members{/member}",
168+
"repositories_url": "https://api.github.com/teams/2/repos",
169+
"parent": null
170+
}
171+
}
172+
]
173+
}

server/api/seats.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,39 @@
11
import { Seat } from "@/model/Seat";
2-
import type FetchError from 'ofetch';
32
import { readFileSync } from 'fs';
43
import { resolve } from 'path';
54

5+
/**
6+
* Deduplicates seats by user ID, keeping the seat with the most recent activity.
7+
* This handles enterprise scenarios where users are assigned to multiple organizations.
8+
* @param seats Array of seats to deduplicate
9+
* @returns Array of unique seats
10+
*/
11+
function deduplicateSeats(seats: Seat[]): Seat[] {
12+
const uniqueSeats = new Map<number, Seat>();
13+
14+
for (const seat of seats) {
15+
// Skip seats with invalid user ID
16+
if (!seat.id || seat.id === 0) {
17+
continue;
18+
}
19+
20+
const existingSeat = uniqueSeats.get(seat.id);
21+
if (!existingSeat) {
22+
uniqueSeats.set(seat.id, seat);
23+
} else {
24+
// Keep the seat with more recent activity, treating null as earliest date
25+
const seatActivity = seat.last_activity_at || '1970-01-01T00:00:00Z';
26+
const existingActivity = existingSeat.last_activity_at || '1970-01-01T00:00:00Z';
27+
28+
if (seatActivity > existingActivity) {
29+
uniqueSeats.set(seat.id, seat);
30+
}
31+
}
32+
}
33+
34+
return Array.from(uniqueSeats.values());
35+
}
36+
637
export default defineEventHandler(async (event) => {
738

839
const logger = console;
@@ -29,9 +60,12 @@ export default defineEventHandler(async (event) => {
2960
const data = readFileSync(path, 'utf8');
3061
const dataJson = JSON.parse(data);
3162
const seatsData = dataJson.seats.map((item: unknown) => new Seat(item));
63+
64+
// Deduplicate seats by user ID to handle enterprise scenarios where users are assigned to multiple organizations
65+
const deduplicatedSeats = deduplicateSeats(seatsData);
3266

3367
logger.info('Using mocked data');
34-
return seatsData;
68+
return deduplicatedSeats;
3569
}
3670

3771
if (!event.context.headers.has('Authorization')) {
@@ -76,5 +110,8 @@ export default defineEventHandler(async (event) => {
76110
seatsData = seatsData.concat(response.seats.map((item: unknown) => new Seat(item)));
77111
}
78112

79-
return seatsData;
113+
// Deduplicate seats by user ID to handle enterprise scenarios where users are assigned to multiple organizations
114+
const deduplicatedSeats = deduplicateSeats(seatsData);
115+
116+
return deduplicatedSeats;
80117
})

0 commit comments

Comments
 (0)