Skip to content

Commit b95994e

Browse files
authored
feat: emeritus command implementation (#1)
1 parent 45760ec commit b95994e

File tree

11 files changed

+559
-1
lines changed

11 files changed

+559
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,4 @@ dist
137137
# Vite logs files
138138
vite.config.js.timestamp-*
139139
vite.config.ts.timestamp-*
140+
.vscode/

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ignore-scripts=true
2+
package-lock=false

LICENSE

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
MIT License
22

3-
Copyright (c) 2025 Fastify
3+
Copyright (c) 2025 The Fastify Team
4+
5+
The Fastify team members are listed at https://github.com/fastify/fastify#team
6+
and in the README file.
47

58
Permission is hereby granted, free of charge, to any person obtaining a copy
69
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,32 @@
11
# org-admin
22
Utilities to handle the organization's permissions
3+
4+
## Installation
5+
6+
```bash
7+
npm install @fastify-org/org-admin
8+
```
9+
10+
## Commands
11+
12+
### Onboard a user
13+
14+
- [ ] TODO
15+
16+
### Offboard a user
17+
18+
- [ ] TODO
19+
20+
### Check emeritus members
21+
22+
This command checks the last contribution date of org's members.
23+
It creates an issue listing the users that have been inactive for more than a specified number of months.
24+
25+
26+
```bash
27+
node --env-file=.env index.js emeritus --org <org> [--monthsInactiveThreshold] [--dryRun]
28+
```
29+
30+
## License
31+
32+
Licensed under [MIT](./LICENSE).

commands/emeritus.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Finds inactive members in an organization for the given number of months
3+
* and opens an issue in the repository to propose moving them to the emeritus team.
4+
* @param {{ client: import('../github-api.js').default, logger: import('pino').Logger }} deps
5+
* @param {{ org: string, monthsInactiveThreshold: number, dryRun: boolean }} options
6+
* @returns {Promise<void>}
7+
*/
8+
export default async function emeritus ({ client, logger }, { org, monthsInactiveThreshold, dryRun }) {
9+
logger.info('Running emeritus command for organization: %s', org)
10+
11+
const orgData = await client.getOrgData(org)
12+
logger.info('Organization ID %s', orgData.id)
13+
14+
const orgTeams = await client.getOrgChart(orgData)
15+
logger.info('Total teams: %s', orgTeams.length)
16+
17+
// This maps holds the teams each member belongs to
18+
const membersOverview = new Map()
19+
for (const team of orgTeams) {
20+
for (const member of team.members) {
21+
if (membersOverview.has(member.login) === false) {
22+
membersOverview.set(member.login, [team])
23+
} else {
24+
membersOverview.get(member.login).push(team)
25+
}
26+
}
27+
}
28+
29+
const membersList = Array.from(membersOverview.keys())
30+
logger.info('Total members: %s', membersList.length)
31+
32+
const yearsToRead = Math.ceil(monthsInactiveThreshold / 12)
33+
const membersContributions = await client.getUsersContributions(orgData, membersList, yearsToRead)
34+
35+
const leadTeam = orgTeams.find(team => team.slug === 'leads')
36+
const usersThatShouldBeEmeritus = membersContributions
37+
.filter(isEmeritus(monthsInactiveThreshold))
38+
.filter(isNotLead(leadTeam))
39+
logger.info('Total emeritus members found: %s', usersThatShouldBeEmeritus.length)
40+
41+
const emeritusTeam = orgTeams.find(team => team.slug === 'emeritus')
42+
const currentEmeritusUsers = emeritusTeam.members.map(member => member.login)
43+
44+
const usersToEmeritus = usersThatShouldBeEmeritus.filter(user => currentEmeritusUsers.includes(user.user) === false)
45+
logger.debug('Total users to move to emeritus team: %s', usersToEmeritus.length)
46+
47+
if (dryRun) {
48+
logger.info('These users should be added to emeritus team:')
49+
usersToEmeritus.forEach(user => logger.info(`- @${user.user}`))
50+
} else {
51+
await client.createIssue(
52+
orgData.name,
53+
'org-admin',
54+
'Move to emeritus members',
55+
`The following users have been inactive for more than ${monthsInactiveThreshold} months
56+
and should be added to the emeritus team to control the access to the Fastify organization:
57+
58+
${usersToEmeritus.map(user => `- @${user.user}`).join('\n')}
59+
60+
\nComment here if you don't want to move them to emeritus team.`,
61+
['question']
62+
)
63+
}
64+
}
65+
66+
function isNotLead (leadTeam) {
67+
const leads = leadTeam.members.map(member => member.login)
68+
return member => !leads.includes(member.user)
69+
}
70+
71+
function isEmeritus (monthsInactiveThreshold) {
72+
const now = new Date()
73+
return function filter (member) {
74+
const dates = [member.lastPR, member.lastIssue, member.lastCommit].filter(Boolean)
75+
// If any contribution is within the threshold, user should NOT be emeritus
76+
return !dates.some(date => {
77+
const monthsDiff = (now.getFullYear() - date.getFullYear()) * 12 + (now.getMonth() - date.getMonth())
78+
return monthsDiff <= monthsInactiveThreshold
79+
})
80+
}
81+
}

commands/offboard.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Offboards a user from an organization.
3+
* @param {{ logger: import('pino').Logger }} deps
4+
* @param {{ org: string, username: string, dryRun: boolean }} options
5+
* @returns {Promise<void>}
6+
*/
7+
export default async function offboard ({ logger }, { username, dryRun }) {
8+
// Implement offboarding logic here
9+
if (dryRun) {
10+
logger.info(`[DRY RUN] Would offboard user: ${username}`)
11+
} else {
12+
logger.info(`Offboarding user: ${username}`)
13+
}
14+
}

commands/onboard.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Onboards a user to an organization.
3+
* @param {{ client: import('../github-api.js').default, logger: import('pino').Logger }} deps
4+
* @param {{ org: string, username: string, dryRun: boolean }} options
5+
* @returns {Promise<void>}
6+
*/
7+
export default async function onboard ({ client, logger }, { org, username, dryRun }) {
8+
const orgId = await client.getOrgId(org)
9+
logger.info('Organization ID %s', orgId)
10+
11+
const orgChart = await client.getOrgChart(org)
12+
13+
// TODO Implement onboarding logic here
14+
if (dryRun) {
15+
logger.info(`[DRY RUN] Would onboard user: ${username}`)
16+
} else {
17+
logger.info(`Onboarding user: ${username}`)
18+
}
19+
}

eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import neostandard from 'neostandard'
2+
3+
export default neostandard({})

0 commit comments

Comments
 (0)