@@ -1471,7 +1471,7 @@ func (*Client) parseGraphQLTimelineEvent(_ /* ctx */ context.Context, item map[s
14711471}
14721472
14731473// writeAccessFromAssociation calculates write access from association.
1474- func (* Client ) writeAccessFromAssociation (_ /* ctx */ context.Context , _ /* owner */ , _ /* repo */ , user , association string ) int {
1474+ func (c * Client ) writeAccessFromAssociation (ctx context.Context , owner , repo , user , association string ) int {
14751475 if user == "" {
14761476 return WriteAccessNA
14771477 }
@@ -1480,17 +1480,71 @@ func (*Client) writeAccessFromAssociation(_ /* ctx */ context.Context, _ /* owne
14801480 case "OWNER" , "COLLABORATOR" :
14811481 return WriteAccessDefinitely
14821482 case "MEMBER" :
1483- // For MEMBER, we'd need an additional API call to check permissions
1484- // This is the one case where GraphQL doesn't give us everything
1485- // For now, return likely
1486- return WriteAccessLikely
1483+ // For MEMBER, check collaborators cache to determine actual permission level
1484+ // Members can have various permissions (admin, write, read) so we need to check
1485+ return c .checkCollaboratorPermission (ctx , owner , repo , user )
14871486 case "CONTRIBUTOR" , "NONE" , "FIRST_TIME_CONTRIBUTOR" , "FIRST_TIMER" :
14881487 return WriteAccessUnlikely
14891488 default :
14901489 return WriteAccessNA
14911490 }
14921491}
14931492
1493+ // checkCollaboratorPermission checks if a user has write access by looking them up in the collaborators list.
1494+ // Uses cache to avoid repeated API calls (4 hour TTL).
1495+ func (c * Client ) checkCollaboratorPermission (ctx context.Context , owner , repo , user string ) int {
1496+ // Check cache first
1497+ if collabs , ok := c .collaboratorsCache .get (owner , repo ); ok {
1498+ switch collabs [user ] {
1499+ case "admin" , "maintain" , "write" :
1500+ return WriteAccessDefinitely
1501+ case "read" , "triage" , "none" :
1502+ return WriteAccessNo
1503+ default :
1504+ // User not in collaborators list
1505+ return WriteAccessUnlikely
1506+ }
1507+ }
1508+
1509+ // Cache miss - fetch collaborators from API
1510+ gc , ok := c .github .(* githubClient )
1511+ if ! ok {
1512+ // Not a real GitHub client (probably test mock) - return likely as fallback
1513+ return WriteAccessLikely
1514+ }
1515+
1516+ collabs , err := gc .collaborators (ctx , owner , repo )
1517+ if err != nil {
1518+ // API call failed (could be 403 if no permission to list collaborators)
1519+ // Return likely as fallback
1520+ c .logger .WarnContext (ctx , "failed to fetch collaborators for write access check" ,
1521+ "owner" , owner ,
1522+ "repo" , repo ,
1523+ "user" , user ,
1524+ "error" , err )
1525+ return WriteAccessLikely
1526+ }
1527+
1528+ // Store in cache
1529+ if err := c .collaboratorsCache .set (owner , repo , collabs ); err != nil {
1530+ // Cache write failed, just log it and continue
1531+ c .logger .WarnContext (ctx , "failed to cache collaborators" ,
1532+ "owner" , owner ,
1533+ "repo" , repo ,
1534+ "error" , err )
1535+ }
1536+
1537+ switch collabs [user ] {
1538+ case "admin" , "maintain" , "write" :
1539+ return WriteAccessDefinitely
1540+ case "read" , "triage" , "none" :
1541+ return WriteAccessNo
1542+ default :
1543+ // User not in collaborators list
1544+ return WriteAccessUnlikely
1545+ }
1546+ }
1547+
14941548// extractRequiredChecksFromGraphQL gets required checks from GraphQL response.
14951549func (* Client ) extractRequiredChecksFromGraphQL (data * graphQLPullRequestComplete ) []string {
14961550 checkMap := make (map [string ]bool )
0 commit comments